From b4bc488fa2eeff5d98c9995900d5db4bd7db8cdb Mon Sep 17 00:00:00 2001 From: itshaseebsaeed Date: Thu, 14 Dec 2023 18:06:38 +0500 Subject: [PATCH] basic staking completed --- .../liquidity_book/lb_pair/src/contract.rs | 15 +- .../staking_contract/src/contract.rs | 124 +++- .../staking_contract/src/helper.rs | 17 +- .../tests/src/multitests/staking_contract.rs | 665 +++++++++++++++++- .../src/interfaces/staking_contract.rs | 145 +++- .../liquidity_book/staking.rs | 45 +- 6 files changed, 947 insertions(+), 64 deletions(-) diff --git a/contracts/liquidity_book/lb_pair/src/contract.rs b/contracts/liquidity_book/lb_pair/src/contract.rs index 3d47e93a..4fb8ab38 100644 --- a/contracts/liquidity_book/lb_pair/src/contract.rs +++ b/contracts/liquidity_book/lb_pair/src/contract.rs @@ -29,7 +29,7 @@ use shade_protocol::{ WasmMsg, }, contract_interfaces::{ - liquidity_book::{lb_pair::*, lb_token}, + liquidity_book::{lb_pair::*, lb_token, staking}, swap::{ amm_pair::{ FeeInfo, @@ -37,7 +37,6 @@ use shade_protocol::{ }, core::{Fee, TokenPair, TokenType}, router::ExecuteMsgResponse, - staking, }, }, lb_libraries::{ @@ -1569,8 +1568,16 @@ fn try_calculate_rewards(deps: DepsMut, env: Env, info: MessageInfo) -> Result StdResult { admin_auth: state.admin_auth.into(), query_auth: None, first_reward_token: None, - epoch_index: 0, //TODO: Set this + epoch_index: 1, //TODO: Set this epoch_duration: 100, //TODO: Set this expiry_duration: None, }; diff --git a/contracts/liquidity_book/staking_contract/src/contract.rs b/contracts/liquidity_book/staking_contract/src/contract.rs index 8084e923..e4f0c4ab 100644 --- a/contracts/liquidity_book/staking_contract/src/contract.rs +++ b/contracts/liquidity_book/staking_contract/src/contract.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, ops::{Add, AddAssign, Sub}, str::FromStr, }; @@ -590,20 +591,16 @@ pub fn try_end_epoch( let mut state = STATE.load(deps.storage)?; //check that only be called by lp-pair assert_lb_pair(&state, info)?; - //saves the distribution - let mut epoch_obj = EPOCH_STORE.load(deps.storage, state.epoch_index)?; - epoch_obj.rewards_distribution = Some(rewards_distribution); - - if let Some(expiry_duration) = epoch_obj.expired_at { - epoch_obj.expired_at = Some(state.epoch_index + expiry_duration) - } - - EPOCH_STORE.save(deps.storage, state.epoch_index, &epoch_obj)?; let mut reward_tokens = vec![]; for reward_token in REWARD_TOKENS.load(deps.storage)? { - let rewards_token_info = REWARD_TOKEN_INFO.load(deps.storage, &reward_token.address)?; + let rewards_token_info; + if let Ok(r_t_i) = REWARD_TOKEN_INFO.load(deps.storage, &reward_token.address) { + rewards_token_info = r_t_i; + } else { + continue; + }; // Create a filtered list of rewards to add to reward_tokens let rewards_to_add: Vec<_> = rewards_token_info @@ -621,13 +618,24 @@ pub fn try_end_epoch( } } + //saves the distribution + let mut epoch_obj = EPOCH_STORE.load(deps.storage, state.epoch_index)?; + epoch_obj.rewards_distribution = Some(rewards_distribution); + epoch_obj.reward_tokens = Some(reward_tokens); + + if let Some(expiry_duration) = epoch_obj.expired_at { + epoch_obj.expired_at = Some(state.epoch_index + expiry_duration) + } + + EPOCH_STORE.save(deps.storage, state.epoch_index, &epoch_obj)?; + let now = env.block.time.seconds(); EPOCH_STORE.save(deps.storage, state.epoch_index.add(1), &EpochInfo { rewards_distribution: None, start_time: now, end_time: now + state.epoch_durations, duration: state.epoch_durations, - reward_tokens: Some(reward_tokens), + reward_tokens: None, expired_at: None, })?; @@ -643,6 +651,8 @@ pub fn try_claim_rewards(deps: DepsMut, _env: Env, info: MessageInfo) -> StdResu let staker_result = STAKERS.load(deps.storage, &info.sender); let mut messages = Vec::new(); + let mut rewards_accumulator: HashMap = HashMap::new(); + // Check if staker exists let staker = match staker_result { Ok(staker) => staker, @@ -690,19 +700,26 @@ pub fn try_claim_rewards(deps: DepsMut, _env: Env, info: MessageInfo) -> StdResu if let Some(rewards_distribution) = &epoch_info.rewards_distribution { for (i, dis) in rewards_distribution.ids.iter().enumerate() { let staker_liq_snap = - finding_user_liquidity(deps.storage, &info, &staker, state.epoch_index, *dis)?; + finding_user_liquidity(deps.storage, &info, &staker, round_epoch, *dis)?; + if staker_liq_snap.liquidity.is_zero() { + continue; + } STAKERS_LIQUIDITY_SNAPSHOT.save( deps.storage, - (&info.sender, state.epoch_index, *dis), + (&info.sender, round_epoch, *dis), &staker_liq_snap, )?; let total_liquidity_snap = - finding_total_liquidity(deps.storage, state.epoch_index, *dis)?; + finding_total_liquidity(deps.storage, round_epoch, *dis)?; + + if total_liquidity_snap.liquidity.is_zero() { + continue; + } TOTAL_LIQUIDITY_SNAPSHOT.save( deps.storage, - (state.epoch_index, *dis), + (round_epoch, *dis), &total_liquidity_snap, )?; @@ -723,26 +740,67 @@ pub fn try_claim_rewards(deps: DepsMut, _env: Env, info: MessageInfo) -> StdResu .to_string(), )?; - messages.push( - Snip20ExecuteMsg::Send { - recipient: info.sender.to_string(), - recipient_code_hash: None, - amount: staker_rewards, - msg: None, - memo: None, - padding: None, - } - .to_cosmos_msg(&x.token, vec![])?, - ); + let token_key = TokenKey { + address: x.token.address.clone(), + code_hash: x.token.code_hash.clone(), + }; + let entry = rewards_accumulator + .entry(token_key) + .or_insert(Uint128::zero()); + *entry += staker_rewards; } } } } } + // Create messages for each token with the accumulated rewards + for (token_key, amount) in rewards_accumulator { + let contract_info = ContractInfo { + address: token_key.address, + code_hash: token_key.code_hash, + }; + messages.push( + Snip20ExecuteMsg::Send { + recipient: info.sender.to_string(), + recipient_code_hash: None, + amount, + msg: None, + memo: None, + padding: None, + } + .to_cosmos_msg(&contract_info, vec![])?, + ); + } + + //TODO: add user rewards somewhere + Ok(Response::default().add_messages(messages)) } +use std::hash::{Hash, Hasher}; + +#[derive(Clone, Debug)] +struct TokenKey { + address: Addr, + code_hash: String, +} + +impl PartialEq for TokenKey { + fn eq(&self, other: &Self) -> bool { + self.address == other.address && self.code_hash == other.code_hash + } +} + +impl Eq for TokenKey {} + +impl Hash for TokenKey { + fn hash(&self, state: &mut H) { + self.address.hash(state); + self.code_hash.hash(state); + } +} + pub fn try_register_reward_tokens( deps: DepsMut, env: Env, @@ -825,12 +883,6 @@ pub fn try_add_rewards( return Err(StdError::generic_err("Cannot start emitting in the past")); } - validate_admin( - &deps.querier, - AdminPermissions::StakingAdmin, - info.sender.to_string(), - &state.admin_auth.into(), - )?; let decimals = token_info(&deps.querier, &Contract { address: token.address.clone(), code_hash: token.code_hash.clone(), @@ -918,6 +970,7 @@ fn try_revoke_permit( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::ContractInfo {} => query_contract_info(deps), + QueryMsg::RegisteredTokens {} => query_registered_tokens(deps), QueryMsg::IdTotalBalance { id } => query_token_id_balance(deps, id), QueryMsg::Balance { .. } | QueryMsg::AllBalances { .. } @@ -943,6 +996,13 @@ fn query_contract_info(deps: Deps) -> StdResult { to_binary(&response) } +fn query_registered_tokens(deps: Deps) -> StdResult { + let reg_tokens = REWARD_TOKENS.load(deps.storage)?; + + let response = QueryAnswer::RegisteredTokens(reg_tokens); + to_binary(&response) +} + fn query_token_id_balance(deps: Deps, token_id: String) -> StdResult { let id = u32::from_str(&token_id) .map_err(|_| StdError::generic_err(format!("token_id {} cannot be parsed", token_id)))?; diff --git a/contracts/liquidity_book/staking_contract/src/helper.rs b/contracts/liquidity_book/staking_contract/src/helper.rs index 375915b9..db8e2eb6 100644 --- a/contracts/liquidity_book/staking_contract/src/helper.rs +++ b/contracts/liquidity_book/staking_contract/src/helper.rs @@ -54,11 +54,11 @@ pub fn register_reward_tokens( tokens: Vec, contract_code_hash: String, ) -> StdResult> { - let mut binding = REWARD_TOKENS.load(storage)?; + let mut reg_tokens = REWARD_TOKENS.load(storage)?; let mut messages = Vec::new(); for token in tokens.iter() { - if !binding.contains(token) { - binding.push(token.clone()); + if !reg_tokens.contains(token) { + reg_tokens.push(token.clone()); let contract = &Contract { address: token.address.to_owned(), @@ -77,9 +77,11 @@ pub fn register_reward_tokens( contract, )?); //set viewing_key + } else { + return Err(StdError::generic_err("Reward token already exists")); } } - REWARD_TOKENS.save(storage, &binding)?; + REWARD_TOKENS.save(storage, ®_tokens)?; Ok(messages) } @@ -168,6 +170,7 @@ pub fn finding_user_liquidity( } else { staker_info.starting_round.unwrap() }; + while finding_liq_round >= start { let staker_liq_snap_prev_round = STAKERS_LIQUIDITY_SNAPSHOT .load(storage, (&info.sender, finding_liq_round, bin_id)) @@ -179,6 +182,8 @@ pub fn finding_user_liquidity( finding_liq_round = if let Some(f_liq_round) = finding_liq_round.checked_sub(1) { f_liq_round } else { + println!("finding_liq_round {:?}", finding_liq_round); + println!("start {:?}", start); return Err(StdError::generic_err("Under-flow sub error 4")); }; } @@ -188,7 +193,7 @@ pub fn finding_user_liquidity( staker_liq_snap.amount_delegated = legacy_bal; // user_liquidity_snapshot_stats_helper_store(storage, round_index, sender, staker_liq_snap)?; - Ok((staker_liq_snap)) + Ok(staker_liq_snap) } } @@ -227,6 +232,8 @@ pub fn finding_total_liquidity( finding_liq_round = if let Some(f_liq_round) = finding_liq_round.checked_sub(1) { f_liq_round } else { + print!("finding_liq_round {:?}", finding_liq_round); + print!("start {:?}", start); return Err(StdError::generic_err("Under-flow sub error 4")); }; } diff --git a/contracts/liquidity_book/tests/src/multitests/staking_contract.rs b/contracts/liquidity_book/tests/src/multitests/staking_contract.rs index ddb2c705..e8123ca8 100644 --- a/contracts/liquidity_book/tests/src/multitests/staking_contract.rs +++ b/contracts/liquidity_book/tests/src/multitests/staking_contract.rs @@ -1,28 +1,20 @@ -use std::{ - ops::{Add, Mul}, - vec, -}; +use std::vec; -use cosmwasm_std::{Timestamp, Uint256}; +use anyhow::Ok; +use cosmwasm_std::{StdError, Timestamp, Uint256}; use ethnum::U256; -use rand::random; use shade_multi_test::interfaces::{ lb_factory, lb_pair, lb_token, + snip20, staking_contract, utils::DeployedContracts, }; use shade_protocol::{ c_std::{to_binary, ContractInfo, Uint128}, - lb_libraries::{ - math::{u24::U24, uint256_to_u256::ConvertU256}, - types::LBPairInformation, - }, - liquidity_book::{ - lb_token::SendAction, - staking::{ExecuteMsg, InvokeMsg}, - }, + lb_libraries::{math::uint256_to_u256::ConvertU256, types::LBPairInformation}, + liquidity_book::{lb_token::SendAction, staking::InvokeMsg}, multi_test::App, }; @@ -246,6 +238,109 @@ pub fn fuzz_stake_simple() -> Result<(), anyhow::Error> { Ok(()) } +#[test] +pub fn fuzz_claim_rewards() -> Result<(), anyhow::Error> { + let x_bins = generate_random(0, 50); + let y_bins = generate_random(0, 50); + + // should be init with the lb-pair + //then query it about the contract info + let addrs = init_addrs(); + let (mut app, lb_factory, deployed_contracts, _lb_pair, _lb_token) = + lb_pair_setup(Some(x_bins), Some(y_bins))?; + + let token_x = extract_contract_info(&deployed_contracts, SHADE)?; + let token_y = extract_contract_info(&deployed_contracts, SILK)?; + + let all_pairs = lb_factory::query_all_lb_pairs( + &mut app, + &lb_factory.clone().into(), + token_x.into(), + token_y.into(), + )?; + let lb_pair = all_pairs[0].clone(); + + let lb_token = lb_pair::query_lb_token(&mut app, &lb_pair.lb_pair.contract)?; + + let staking_contract = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + + //deposit funds here + let total_bins = get_total_bins(x_bins, y_bins) as u32; + + let mut actions = vec![]; + let mut ids = vec![]; + //Querying all the bins + for i in 0..total_bins { + let id = get_id(ACTIVE_ID, i, y_bins); + ids.push(id); + + let balance = lb_token::query_balance( + &app, + &lb_token, + addrs.batman(), + addrs.batman(), + String::from("viewing_key"), + id.to_string(), + )?; + + actions.push(SendAction { + token_id: id.to_string(), + from: addrs.batman(), + recipient: staking_contract.address.clone(), + recipient_code_hash: Some(staking_contract.code_hash.clone()), + amount: balance, + msg: Some(to_binary(&InvokeMsg::Stake { + from: Some(addrs.batman().to_string()), + padding: None, + })?), + memo: None, + }) + } + + lb_token::batch_send(&mut app, addrs.batman().as_str(), &lb_token, actions)?; + + let shade_token = extract_contract_info(&deployed_contracts, SHADE)?; + let silk_token = extract_contract_info(&deployed_contracts, SILK)?; + + let reward_tokens = vec![shade_token, silk_token]; + + staking_contract::register_reward_tokens( + &mut app, + addrs.admin().as_str(), + &staking_contract, + reward_tokens.clone(), + )?; + + //mint tokens + snip20::mint_exec( + &mut app, + addrs.admin().as_str(), + &deployed_contracts, + SHADE, + &vec![], + addrs.admin().to_string(), + DEPOSIT_AMOUNT.into(), + )?; + + snip20::send_exec( + &mut app, + addrs.admin().as_str(), + &deployed_contracts, + SHADE, + staking_contract.address.to_string(), + DEPOSIT_AMOUNT.into(), + Some(to_binary(&InvokeMsg::AddRewards { + start: None, + end: 11, + })?), + )?; + + lb_pair::calculate_rewards(&mut app, addrs.admin().as_str(), &lb_pair.lb_pair.contract)?; + + staking_contract::claim_rewards(&mut app, addrs.batman().as_str(), &staking_contract)?; + + Ok(()) +} #[test] pub fn fuzz_stake_liquidity_with_time() -> Result<(), anyhow::Error> { @@ -541,3 +636,545 @@ pub fn fuzz_unstake() -> Result<(), anyhow::Error> { Ok(()) } + +#[test] +pub fn fuzz_unstake_liquidity_with_time() -> Result<(), anyhow::Error> { + let x_bins = generate_random(0, 50); + let y_bins = generate_random(0, 50); + // should be init with the lb-pair + //then query it about the contract info + let addrs = init_addrs(); + let (mut app, lb_factory, deployed_contracts, _lb_pair, _lb_token) = + lb_pair_setup(Some(x_bins), Some(y_bins))?; + + let token_x = extract_contract_info(&deployed_contracts, SHADE)?; + let token_y = extract_contract_info(&deployed_contracts, SILK)?; + + let all_pairs = lb_factory::query_all_lb_pairs( + &mut app, + &lb_factory.clone().into(), + token_x.into(), + token_y.into(), + )?; + let lb_pair = all_pairs[0].clone(); + + let lb_token = lb_pair::query_lb_token(&mut app, &lb_pair.lb_pair.contract)?; + + let staking_contract = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + + //deposit funds here + + let total_bins = get_total_bins(x_bins, y_bins) as u32; + + let mut actions = vec![]; + let mut balances: Vec = Vec::new(); + let mut ids: Vec = Vec::new(); + //Querying all the bins + for i in 0..total_bins { + let id = get_id(ACTIVE_ID, i, y_bins); + ids.push(id); + + let balance = lb_token::query_balance( + &app, + &lb_token, + addrs.batman(), + addrs.batman(), + String::from("viewing_key"), + id.to_string(), + )?; + balances.push(balance); + + actions.push(SendAction { + token_id: id.to_string(), + from: addrs.batman(), + recipient: staking_contract.address.clone(), + recipient_code_hash: Some(staking_contract.code_hash.clone()), + amount: balance, + msg: Some(to_binary(&InvokeMsg::Stake { + from: Some(addrs.batman().to_string()), + padding: None, + })?), + memo: None, + }) + } + + lb_token::batch_send(&mut app, addrs.batman().as_str(), &lb_token, actions)?; + + //removing liquidity after half duration + let timestamp = Timestamp::from_seconds(app.block_info().time.seconds() + 50); + app.set_time(timestamp); + + let owner_balance = lb_token::query_all_balances( + &mut app, + &lb_token, + addrs.batman(), + String::from("viewing_key"), + )?; + + assert_eq!(owner_balance.len(), 0); + + // unstaking + staking_contract::unstaking( + &mut app, + addrs.batman().as_str(), + &staking_contract, + ids.clone(), + balances.clone(), + )?; + + staking_contract::set_viewing_key( + &mut app, + addrs.batman().as_str(), + &staking_contract, + "viewing_key".to_owned(), + )?; + + //Check the liquidity after full time of duration - duration is 100 liquidity won't change + let liquidity = staking_contract::query_liquidity( + &app, + &addrs.batman(), + String::from("viewing_key"), + &staking_contract, + ids.clone(), + None, + )?; + + for (liq, bal) in liquidity.into_iter().zip(balances.clone()).into_iter() { + assert_approx_eq_abs( + liq.user_liquidity, + bal.multiply_ratio(50u128, 100u128), + Uint256::from(1u128), + "ERRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + ); + } + + Ok(()) +} + +#[test] +pub fn register_rewards_token() -> Result<(), anyhow::Error> { + let addrs = init_addrs(); + let (mut app, _lb_factory, deployed_contracts, lb_pair, _lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + + //Add the token + let shade_token = extract_contract_info(&deployed_contracts, SHADE)?; + let silk_token = extract_contract_info(&deployed_contracts, SILK)?; + + let reward_tokens = vec![shade_token, silk_token]; + + staking_contract::register_reward_tokens( + &mut app, + addrs.admin().as_str(), + &lb_staking, + reward_tokens.clone(), + )?; + + //query to check the tokens in there + let q_reward_tokens = staking_contract::query_registered_tokens(&app, &lb_staking)?; + + assert_eq!(q_reward_tokens, reward_tokens); + + //getting an error for trying to add a token again + let res = staking_contract::register_reward_tokens( + &mut app, + addrs.admin().as_str(), + &lb_staking, + reward_tokens, + ); + + assert_eq!( + res.unwrap_err(), + StdError::generic_err("Generic error: Reward token already exists") + ); + + Ok(()) +} + +#[test] +pub fn add_rewards() -> Result<(), anyhow::Error> { + let addrs = init_addrs(); + let (mut app, _lb_factory, deployed_contracts, lb_pair, _lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + + //Add the token + let shade_token = extract_contract_info(&deployed_contracts, SHADE)?; + let silk_token = extract_contract_info(&deployed_contracts, SILK)?; + + let reward_tokens = vec![shade_token, silk_token]; + + staking_contract::register_reward_tokens( + &mut app, + addrs.admin().as_str(), + &lb_staking, + reward_tokens.clone(), + )?; + + //mint tokens + snip20::mint_exec( + &mut app, + addrs.admin().as_str(), + &deployed_contracts, + SHADE, + &vec![], + addrs.admin().to_string(), + DEPOSIT_AMOUNT.into(), + )?; + + snip20::send_exec( + &mut app, + addrs.admin().as_str(), + &deployed_contracts, + SHADE, + lb_staking.address.to_string(), + DEPOSIT_AMOUNT.into(), + Some(to_binary(&InvokeMsg::AddRewards { + start: None, + end: 20, + })?), + )?; + + Ok(()) +} + +#[test] +pub fn end_epoch() -> Result<(), anyhow::Error> { + let addrs = init_addrs(); + let (mut app, _lb_factory, deployed_contracts, lb_pair, _lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + + //Add the token + let shade_token = extract_contract_info(&deployed_contracts, SHADE)?; + let silk_token = extract_contract_info(&deployed_contracts, SILK)?; + + let reward_tokens = vec![shade_token, silk_token]; + + staking_contract::register_reward_tokens( + &mut app, + addrs.admin().as_str(), + &lb_staking, + reward_tokens.clone(), + )?; + + //mint tokens + snip20::mint_exec( + &mut app, + addrs.admin().as_str(), + &deployed_contracts, + SHADE, + &vec![], + addrs.admin().to_string(), + DEPOSIT_AMOUNT.into(), + )?; + + snip20::send_exec( + &mut app, + addrs.admin().as_str(), + &deployed_contracts, + SHADE, + lb_staking.address.to_string(), + DEPOSIT_AMOUNT.into(), + Some(to_binary(&InvokeMsg::AddRewards { + start: None, + end: 20, + })?), + )?; + + lb_pair::calculate_rewards(&mut app, addrs.admin().as_str(), &lb_pair.lb_pair.contract)?; + + Ok(()) +} + +#[test] +pub fn claim_rewards() -> Result<(), anyhow::Error> { + let addrs = init_addrs(); + let x_bins = generate_random(0, 50); + let y_bins = generate_random(0, 50); + let (mut app, _lb_factory, _deployed_contracts, lb_pair, _lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + + let lb_token = lb_pair::query_lb_token(&mut app, &lb_pair.lb_pair.contract)?; + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + + //deposit funds here + let total_bins = get_total_bins(x_bins, y_bins) as u32; + + let mut actions = vec![]; + //Querying all the bins + for i in 0..total_bins { + let id = get_id(ACTIVE_ID, i, y_bins); + + let balance = lb_token::query_balance( + &app, + &lb_token, + addrs.batman(), + addrs.batman(), + String::from("viewing_key"), + id.to_string(), + )?; + + actions.push(SendAction { + token_id: id.to_string(), + from: addrs.batman(), + recipient: lb_staking.address.clone(), + recipient_code_hash: Some(lb_staking.code_hash.clone()), + amount: balance, + msg: Some(to_binary(&InvokeMsg::Stake { + from: Some(addrs.batman().to_string()), + padding: None, + })?), + memo: None, + }) + } + + Ok(()) +} + +#[test] +pub fn update_config() -> Result<(), anyhow::Error> { + let addrs = init_addrs(); + let (mut app, _lb_factory, _deployed_contracts, lb_pair, _lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + + let config = staking_contract::query_config(&app, &lb_staking)?; + assert_eq!(config.epoch_durations, (100)); + + staking_contract::update_config( + &mut app, + addrs.admin().as_str(), + &lb_staking, + None, + None, + Some(200), + None, + )?; + let config = staking_contract::query_config(&app, &lb_staking)?; + assert_eq!(config.epoch_durations, (200)); + + Ok(()) +} + +#[test] +fn query_contract_info() -> Result<(), anyhow::Error> { + let (mut app, _lb_factory, _deployed_contracts, lb_pair, lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + + let config = staking_contract::query_config(&app, &lb_staking)?; + + assert_eq!(config.lb_pair, lb_pair.lb_pair.contract.address); + assert_eq!(config.lb_token.address, lb_token.address); + + Ok(()) +} + +#[test] +fn query_id_balance() -> Result<(), anyhow::Error> { + let addrs = init_addrs(); + let (mut app, _lb_factory, _deployed_contracts, lb_pair, lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + let total_bins = get_total_bins(NB_BINS_X, NB_BINS_Y) as u32; + + //stake: + let mut actions = vec![]; + + for i in 0..total_bins { + let id = get_id(ACTIVE_ID, i, NB_BINS_Y); + + let balance = lb_token::query_balance( + &app, + &lb_token, + addrs.batman(), + addrs.batman(), + String::from("viewing_key"), + id.to_string(), + )?; + + actions.push(SendAction { + token_id: id.to_string(), + from: addrs.batman(), + recipient: lb_staking.address.clone(), + recipient_code_hash: Some(lb_staking.code_hash.clone()), + amount: balance, + msg: Some(to_binary(&InvokeMsg::Stake { + from: Some(addrs.batman().to_string()), + padding: None, + })?), + memo: None, + }) + } + + lb_token::batch_send(&mut app, addrs.batman().as_str(), &lb_token, actions)?; + + for i in 0..total_bins { + let id = get_id(ACTIVE_ID, i, NB_BINS_Y); + let (reserves_x, reserves_y) = lb_pair::query_bin(&app, &lb_pair.lb_pair.contract, id)?; + let price = lb_pair::query_price_from_id(&app, &lb_pair.lb_pair.contract, id)?; + + let expected_balance_x = U256::from(reserves_x); + let expected_balance_y = U256::from(reserves_y); + + let total: U256 = expected_balance_x * U256::from_str_prefixed(price.to_string().as_str())? + + (expected_balance_y << 128); + + let balance = staking_contract::query_id_total_balance(&app, &lb_staking, id)?; + + assert_eq!(total.u256_to_uint256(), balance); + assert!(balance > Uint256::MIN); + } + + Ok(()) +} + +#[test] +fn query_balance() -> Result<(), anyhow::Error> { + let addrs = init_addrs(); + let (mut app, _lb_factory, _deployed_contracts, lb_pair, lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + let total_bins = get_total_bins(NB_BINS_X, NB_BINS_Y) as u32; + + //stake: + let mut actions = vec![]; + + for i in 0..total_bins { + let id = get_id(ACTIVE_ID, i, NB_BINS_Y); + + let balance = lb_token::query_balance( + &app, + &lb_token, + addrs.batman(), + addrs.batman(), + String::from("viewing_key"), + id.to_string(), + )?; + + actions.push(SendAction { + token_id: id.to_string(), + from: addrs.batman(), + recipient: lb_staking.address.clone(), + recipient_code_hash: Some(lb_staking.code_hash.clone()), + amount: balance, + msg: Some(to_binary(&InvokeMsg::Stake { + from: Some(addrs.batman().to_string()), + padding: None, + })?), + memo: None, + }) + } + + lb_token::batch_send(&mut app, addrs.batman().as_str(), &lb_token, actions)?; + + staking_contract::set_viewing_key( + &mut app, + addrs.batman().as_str(), + &lb_staking, + "viewing_key".to_owned(), + )?; + + for i in 0..total_bins { + let id = get_id(ACTIVE_ID, i, NB_BINS_Y); + let (reserves_x, reserves_y) = lb_pair::query_bin(&app, &lb_pair.lb_pair.contract, id)?; + let price = lb_pair::query_price_from_id(&app, &lb_pair.lb_pair.contract, id)?; + + let expected_balance_x = U256::from(reserves_x); + let expected_balance_y = U256::from(reserves_y); + + let total: U256 = expected_balance_x * U256::from_str_prefixed(price.to_string().as_str())? + + (expected_balance_y << 128); + + let balance = staking_contract::query_balance( + &app, + &lb_staking, + addrs.batman(), + "viewing_key".to_owned(), + id, + )?; + + assert_eq!(total.u256_to_uint256(), balance); + assert!(balance > Uint256::MIN); + } + + Ok(()) +} + +#[test] +fn query_all_balance() -> Result<(), anyhow::Error> { + let addrs = init_addrs(); + let (mut app, _lb_factory, _deployed_contracts, lb_pair, lb_token) = + lb_pair_setup(Some(NB_BINS_X), Some(NB_BINS_Y))?; + + let lb_staking = lb_pair::query_staking_contract(&mut app, &lb_pair.lb_pair.contract)?; + let total_bins = get_total_bins(NB_BINS_X, NB_BINS_Y) as u32; + + //stake: + let mut actions = vec![]; + + for i in 0..total_bins { + let id = get_id(ACTIVE_ID, i, NB_BINS_Y); + + let balance = lb_token::query_balance( + &app, + &lb_token, + addrs.batman(), + addrs.batman(), + String::from("viewing_key"), + id.to_string(), + )?; + + actions.push(SendAction { + token_id: id.to_string(), + from: addrs.batman(), + recipient: lb_staking.address.clone(), + recipient_code_hash: Some(lb_staking.code_hash.clone()), + amount: balance, + msg: Some(to_binary(&InvokeMsg::Stake { + from: Some(addrs.batman().to_string()), + padding: None, + })?), + memo: None, + }) + } + + lb_token::batch_send(&mut app, addrs.batman().as_str(), &lb_token, actions)?; + + staking_contract::set_viewing_key( + &mut app, + addrs.batman().as_str(), + &lb_staking, + "viewing_key".to_owned(), + )?; + + let balances = staking_contract::query_all_balances( + &app, + &lb_staking, + addrs.batman(), + "viewing_key".to_owned(), + None, + None, + )?; + + for owner_balance in balances { + let id = owner_balance.token_id.parse().unwrap(); + let (reserves_x, reserves_y) = lb_pair::query_bin(&app, &lb_pair.lb_pair.contract, id)?; + let price = lb_pair::query_price_from_id(&app, &lb_pair.lb_pair.contract, id)?; + + let expected_balance_x = U256::from(reserves_x); + let expected_balance_y = U256::from(reserves_y); + + let total: U256 = expected_balance_x * U256::from_str_prefixed(price.to_string().as_str())? + + (expected_balance_y << 128); + assert_eq!(total.u256_to_uint256(), owner_balance.amount); + assert!(owner_balance.amount > Uint256::MIN); + } + + Ok(()) +} diff --git a/packages/multi_test/src/interfaces/staking_contract.rs b/packages/multi_test/src/interfaces/staking_contract.rs index aba2b80b..9f43e9d2 100644 --- a/packages/multi_test/src/interfaces/staking_contract.rs +++ b/packages/multi_test/src/interfaces/staking_contract.rs @@ -1,8 +1,9 @@ use shade_protocol::{ c_std::{Addr, ContractInfo, StdError, StdResult, Uint256}, - liquidity_book::staking::{ExecuteMsg, Liquidity, QueryAnswer, QueryMsg}, + cosmwasm_schema::cw_serde, + liquidity_book::staking::{ExecuteMsg, Liquidity, OwnerBalance, QueryAnswer, QueryMsg}, multi_test::App, - utils::{ExecuteCallback, Query}, + utils::{asset::RawContract, ExecuteCallback, Query}, }; pub fn set_viewing_key( @@ -40,6 +41,52 @@ pub fn unstaking( } } +pub fn update_config( + app: &mut App, + sender: &str, + lb_staking: &ContractInfo, + admin_auth: Option, + query_auth: Option, + epoch_duration: Option, + expiry_duration: Option, +) -> StdResult<()> { + match (ExecuteMsg::UpdateConfig { + admin_auth, + query_auth, + epoch_duration, + expiry_duration, + } + .test_exec(lb_staking, app, Addr::unchecked(sender), &[])) + { + Ok(_) => Ok(()), + Err(e) => return Err(StdError::generic_err(e.root_cause().to_string())), + } +} + +pub fn claim_rewards(app: &mut App, sender: &str, lb_staking: &ContractInfo) -> StdResult<()> { + match (ExecuteMsg::ClaimRewards {}.test_exec(lb_staking, app, Addr::unchecked(sender), &[])) { + Ok(_) => Ok(()), + Err(e) => return Err(StdError::generic_err(e.root_cause().to_string())), + } +} + +pub fn register_reward_tokens( + app: &mut App, + sender: &str, + lb_staking: &ContractInfo, + tokens: Vec, +) -> StdResult<()> { + match ExecuteMsg::RegisterRewardTokens(tokens).test_exec( + lb_staking, + app, + Addr::unchecked(sender), + &[], + ) { + Ok(_) => Ok(()), + Err(e) => return Err(StdError::generic_err(e.root_cause().to_string())), + } +} + pub fn query_liquidity( app: &App, sender: &Addr, @@ -60,3 +107,97 @@ pub fn query_liquidity( _ => Err(StdError::generic_err("Query failed")), } } + +pub fn query_registered_tokens( + app: &App, + lb_staking: &ContractInfo, +) -> StdResult> { + let res: QueryAnswer = QueryMsg::RegisteredTokens {}.test_query(&lb_staking, app)?; + match res { + QueryAnswer::RegisteredTokens(liq) => Ok(liq), + _ => Err(StdError::generic_err("Query failed")), + } +} + +pub fn query_id_total_balance(app: &App, lb_staking: &ContractInfo, id: u32) -> StdResult { + let res: QueryAnswer = + QueryMsg::IdTotalBalance { id: id.to_string() }.test_query(&lb_staking, app)?; + match res { + QueryAnswer::IdTotalBalance { amount } => Ok(amount), + _ => Err(StdError::generic_err("Query failed")), + } +} + +pub fn query_balance( + app: &App, + lb_staking: &ContractInfo, + staker: Addr, + key: String, + id: u32, +) -> StdResult { + let res: QueryAnswer = QueryMsg::Balance { + owner: staker, + key, + token_id: id.to_string(), + } + .test_query(&lb_staking, app)?; + match res { + QueryAnswer::Balance { amount } => Ok(amount), + _ => Err(StdError::generic_err("Query failed")), + } +} + +pub fn query_all_balances( + app: &App, + lb_staking: &ContractInfo, + staker: Addr, + key: String, + page: Option, + page_size: Option, +) -> StdResult> { + let res: QueryAnswer = QueryMsg::AllBalances { + owner: staker, + key, + page, + page_size, + } + .test_query(&lb_staking, app)?; + match res { + QueryAnswer::AllBalances(balances) => Ok(balances), + _ => Err(StdError::generic_err("Query failed")), + } +} + +pub fn query_config(app: &App, lb_staking: &ContractInfo) -> StdResult { + let res: QueryAnswer = QueryMsg::ContractInfo {}.test_query(&lb_staking, app)?; + match res { + QueryAnswer::ContractInfo { + lb_token, + lb_pair, + admin_auth, + query_auth, + epoch_index, + epoch_durations, + expiry_durations, + } => Ok((Config { + lb_token, + lb_pair, + admin_auth, + query_auth, + epoch_index, + epoch_durations, + expiry_durations, + })), + _ => Err(StdError::generic_err("Query failed")), + } +} + +pub struct Config { + pub lb_token: ContractInfo, + pub lb_pair: Addr, + pub admin_auth: ContractInfo, + pub query_auth: Option, + pub epoch_index: u64, + pub epoch_durations: u64, + pub expiry_durations: Option, +} diff --git a/packages/shade_protocol/src/contract_interfaces/liquidity_book/staking.rs b/packages/shade_protocol/src/contract_interfaces/liquidity_book/staking.rs index 0d792f57..a0587519 100644 --- a/packages/shade_protocol/src/contract_interfaces/liquidity_book/staking.rs +++ b/packages/shade_protocol/src/contract_interfaces/liquidity_book/staking.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use cosmwasm_schema::QueryResponses; -use cosmwasm_std::{BlockInfo, StdResult}; +use cosmwasm_std::{to_binary, BlockInfo, Coin, CosmosMsg, StdResult, WasmMsg}; use secret_toolkit::permit::Permit; use crate::{ @@ -14,7 +14,10 @@ use crate::{ BLOCK_SIZE, }; -use super::{lb_pair::RewardsDistribution, lb_token::Snip1155ReceiveMsg}; +use super::{ + lb_pair::RewardsDistribution, + lb_token::{space_pad, Snip1155ReceiveMsg}, +}; impl InstantiateCallback for InstantiateMsg { const BLOCK_SIZE: usize = BLOCK_SIZE; @@ -304,12 +307,12 @@ impl Default for TotalLiquiditySnapshot { pub enum QueryMsg { /// returns public information of the SNIP1155 contract ContractInfo {}, + RegisteredTokens {}, IdTotalBalance { id: String, }, Balance { owner: Addr, - viewer: Addr, key: String, token_id: String, }, @@ -340,12 +343,13 @@ pub enum QueryMsg { impl QueryMsg { pub fn get_validation_params(&self) -> StdResult<(Vec<&Addr>, String)> { match self { - Self::Balance { - owner, viewer, key, .. - } => Ok((vec![owner, viewer], key.clone())), + Self::Balance { owner, key, .. } => Ok((vec![owner], key.clone())), Self::AllBalances { owner, key, .. } => Ok((vec![owner], key.clone())), Self::Liquidity { owner, key, .. } => Ok((vec![owner], key.clone())), - Self::ContractInfo {} | Self::IdTotalBalance { .. } | Self::WithPermit { .. } => { + Self::ContractInfo {} + | Self::IdTotalBalance { .. } + | Self::RegisteredTokens { .. } + | Self::WithPermit { .. } => { unreachable!("This query type does not require viewing key authentication") } Self::TransactionHistory { address, key, .. } => Ok((vec![address], key.clone())), @@ -382,6 +386,7 @@ pub enum QueryAnswer { epoch_durations: u64, expiry_durations: Option, }, + RegisteredTokens(Vec), IdTotalBalance { amount: Uint256, }, @@ -439,3 +444,29 @@ pub struct OwnerBalance { pub token_id: String, pub amount: Uint256, } + +impl ExecuteMsg { + pub fn to_cosmos_msg( + &self, + code_hash: String, + contract_addr: String, + send_amount: Option, + ) -> StdResult { + let mut msg = to_binary(self)?; + space_pad(256, &mut msg.0); + let mut funds = Vec::new(); + if let Some(amount) = send_amount { + funds.push(Coin { + amount, + denom: String::from("uscrt"), + }); + } + let execute = WasmMsg::Execute { + contract_addr, + code_hash, + msg, + funds, + }; + Ok(execute.into()) + } +}