From 3e9371a2a8e8f21426887ffd7f2548c25f70ce8a Mon Sep 17 00:00:00 2001 From: taiki1frsh Date: Fri, 22 Sep 2023 20:15:38 +0900 Subject: [PATCH 1/4] add time window validation for the delete action --- contracts/yieldaggregator-bonus/src/error.rs | 6 ++++++ .../src/execute/delete_bonus_window.rs | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/contracts/yieldaggregator-bonus/src/error.rs b/contracts/yieldaggregator-bonus/src/error.rs index a5ab044..3058289 100644 --- a/contracts/yieldaggregator-bonus/src/error.rs +++ b/contracts/yieldaggregator-bonus/src/error.rs @@ -35,6 +35,12 @@ pub enum ContractError { #[error("Already staked")] AlreadyStaked {}, + + #[error("Insufficient budget to register bonus window")] + InsufficientBudget {}, + + #[error("Bonus window not ended yet")] + BonusWindowNotEndedYet } impl From for ContractError { diff --git a/contracts/yieldaggregator-bonus/src/execute/delete_bonus_window.rs b/contracts/yieldaggregator-bonus/src/execute/delete_bonus_window.rs index efc4998..f696929 100644 --- a/contracts/yieldaggregator-bonus/src/execute/delete_bonus_window.rs +++ b/contracts/yieldaggregator-bonus/src/execute/delete_bonus_window.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; #[cfg(not(feature = "library"))] pub fn execute_delete_bonus_window( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, msg: DeleteBonusWindowMsg, ) -> Result { @@ -19,6 +19,13 @@ pub fn execute_delete_bonus_window( return Err(ContractError::Unauthorized {}); } + let bonus_window = BONUS_WINDOWS.load(deps.storage, msg.bonus_window_id)?; + + // Check if the bonus window is already ended + if env.block.time < bonus_window.end_at { + return Err(ContractError::BonusWindowNotEndedYet {}); + } + BONUS_WINDOWS.remove(deps.storage, msg.bonus_window_id); response = response.add_attribute("action", "delete_bonus_window"); From 14d8aa8f9b22ce9c7493e1b01d8b501f1156cf38 Mon Sep 17 00:00:00 2001 From: taiki1frsh Date: Fri, 22 Sep 2023 20:15:59 +0900 Subject: [PATCH 2/4] update the latest id determination --- .../src/execute/register_bonus_window.rs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/contracts/yieldaggregator-bonus/src/execute/register_bonus_window.rs b/contracts/yieldaggregator-bonus/src/execute/register_bonus_window.rs index 23bc566..ee7ec64 100644 --- a/contracts/yieldaggregator-bonus/src/execute/register_bonus_window.rs +++ b/contracts/yieldaggregator-bonus/src/execute/register_bonus_window.rs @@ -12,6 +12,8 @@ pub fn execute_register_bonus_window( info: MessageInfo, msg: RegisterBonusWindowMsg, ) -> Result { + use cw_utils::one_coin; + let mut response = Response::new(); let params = PARAMS.load(deps.storage)?; @@ -20,17 +22,33 @@ pub fn execute_register_bonus_window( return Err(ContractError::Unauthorized {}); } - let id = 0u64; + let deposit = one_coin(&info)?; + if deposit.denom != msg.denom { + return Err(ContractError::NoAllowedToken {}); + } + + if deposit.amount < msg.budget_for_all { + return Err(ContractError::InsufficientBudget {}); + } + + let latest_id = BONUS_WINDOWS.last(deps.storage) + .map(|res| + match res { + Some((_, bw)) => bw.id.checked_add(1).unwrap(), + None => 0, + } + ) + .unwrap_or(0); let bonus_window = BonusWindow { - id: id, + id: latest_id, denom: msg.denom, budget_for_all: msg.budget_for_all, apr_for_winners: msg.apr_for_winners, start_at: msg.start_at, end_at: msg.end_at, }; - BONUS_WINDOWS.save(deps.storage, id, &bonus_window)?; + BONUS_WINDOWS.save(deps.storage, latest_id, &bonus_window)?; response = response.add_attribute("action", "register_bonus_window"); From e52a41cce7b0a8b1724648686f4fa9d41b07f89e Mon Sep 17 00:00:00 2001 From: taiki1frsh Date: Fri, 22 Sep 2023 20:16:42 +0900 Subject: [PATCH 3/4] add specific bonus window query --- contracts/yieldaggregator-bonus/src/query/bonus_windows.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/yieldaggregator-bonus/src/query/bonus_windows.rs b/contracts/yieldaggregator-bonus/src/query/bonus_windows.rs index 3975200..f92ebdd 100644 --- a/contracts/yieldaggregator-bonus/src/query/bonus_windows.rs +++ b/contracts/yieldaggregator-bonus/src/query/bonus_windows.rs @@ -11,3 +11,9 @@ pub fn query_bonus_windows(deps: Deps) -> StdResult> { .collect::>>()?; Ok(bonus_windows) } + +#[cfg(not(feature = "library"))] +pub fn query_bonus_window(deps: Deps, id: u64) -> StdResult { + let bonus_window = BONUS_WINDOWS.load(deps.storage, id)?; + Ok(bonus_window) +} From 4be22814c6b42deaa428e66c17996a68211097e9 Mon Sep 17 00:00:00 2001 From: taiki1frsh Date: Fri, 22 Sep 2023 20:16:51 +0900 Subject: [PATCH 4/4] unit test --- .../tests/delete_bonus_window.rs | 72 +++++++++++ .../tests/register_bonus_window.rs | 103 +++++++++++++++ .../tests/stake_vault_share.rs | 117 ++++++++++++++++++ .../tests/update_params.rs | 19 ++- contracts/yieldaggregator-bonus/tests/vote.rs | 112 +++++++++++++++++ 5 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 contracts/yieldaggregator-bonus/tests/delete_bonus_window.rs create mode 100644 contracts/yieldaggregator-bonus/tests/register_bonus_window.rs create mode 100644 contracts/yieldaggregator-bonus/tests/stake_vault_share.rs create mode 100644 contracts/yieldaggregator-bonus/tests/vote.rs diff --git a/contracts/yieldaggregator-bonus/tests/delete_bonus_window.rs b/contracts/yieldaggregator-bonus/tests/delete_bonus_window.rs new file mode 100644 index 0000000..0aa211f --- /dev/null +++ b/contracts/yieldaggregator-bonus/tests/delete_bonus_window.rs @@ -0,0 +1,72 @@ +use crate::helpers::setup; +use cosmwasm_std::{testing::{mock_env, mock_info}, Uint128, Decimal, Timestamp, coins}; +use yieldaggregator_bonus::{ + error::ContractError, execute::{register_bonus_window::execute_register_bonus_window, vote::execute_vote, delete_bonus_window::execute_delete_bonus_window}, msgs::{UpdateParamsMsg, RegisterBonusWindowMsg, VoteMsg, DeleteBonusWindowMsg}, + query::{params::query_params, voted_vaults::query_voted_vaults, bonus_windows::{query_bonus_windows, query_bonus_window}}, +}; + +mod helpers; + +#[test] +fn test_delete_bonus_window() { + let mut deps = setup(); + // first, register the bonus window + let bw_denom = format!("yieldaggregator/vault/{}", "0"); + execute_register_bonus_window( + deps.as_mut(), + mock_env(), + mock_info("authority", &coins(100, bw_denom.clone())), + RegisterBonusWindowMsg{ + denom: bw_denom.clone(), + budget_for_all: Uint128::zero(), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: Timestamp::default().plus_seconds(1), + }, + ).unwrap(); + + // Error due to invalid sender + { + let msg = DeleteBonusWindowMsg { + bonus_window_id: 0, + }; + + let info = mock_info( + "anyone", + &coins(100, bw_denom.clone()) + ); + let err = execute_delete_bonus_window( + deps.as_mut(), + mock_env(), + info, + msg, + ).unwrap_err(); + + assert_eq!(ContractError::Unauthorized {}, err); + } + + // Success + { + let msg = DeleteBonusWindowMsg { + bonus_window_id: 0, + }; + + let info = mock_info( + "authority", + &coins(100, bw_denom.clone()) + ); + let res = execute_delete_bonus_window( + deps.as_mut(), + mock_env(), + info, + msg, + ).unwrap(); + + assert_eq!(0, res.messages.len()); + + // Assume that the bonus window is deleted and returns err + let bonus_windows = query_bonus_windows(deps.as_ref()); + assert_eq!(0, bonus_windows.unwrap().len()); + query_bonus_window(deps.as_ref(), 0).unwrap_err(); + } +} diff --git a/contracts/yieldaggregator-bonus/tests/register_bonus_window.rs b/contracts/yieldaggregator-bonus/tests/register_bonus_window.rs new file mode 100644 index 0000000..fca5968 --- /dev/null +++ b/contracts/yieldaggregator-bonus/tests/register_bonus_window.rs @@ -0,0 +1,103 @@ +use crate::helpers::setup; +use cosmwasm_std::{testing::{mock_env, mock_info}, Uint128, Decimal, Timestamp, Coin, coins}; +use yieldaggregator_bonus::{ + error::ContractError, execute::{register_bonus_window::execute_register_bonus_window}, msgs::{UpdateParamsMsg, RegisterBonusWindowMsg, self}, + query::{params::query_params, bonus_windows::{query_bonus_windows, query_bonus_window}}, +}; + +mod helpers; + +#[test] +fn test_register_bonus_window() { + let mut deps = setup(); + + // Error due to the permission + { + let invalid_info = mock_info("anyone", &[]); + let msg = RegisterBonusWindowMsg { + denom: "tst".to_string(), + budget_for_all: Uint128::zero(), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: Timestamp::default(), + }; + let err = execute_register_bonus_window( + deps.as_mut(), + mock_env(), + invalid_info, + msg, + ).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + + // Error due to the insufficient budget + { + let info = mock_info("authority", &coins(10, "tst")); + let msg = RegisterBonusWindowMsg { + denom: "tst".to_string(), + budget_for_all: Uint128::new(100), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: Timestamp::default(), + }; + let err = execute_register_bonus_window( + deps.as_mut(), + mock_env(), + info, + msg, + ).unwrap_err(); + assert_eq!(err, ContractError::InsufficientBudget {}); + } + + // Success + { + let info = mock_info("authority", &coins(100, "tst")); + let msg = RegisterBonusWindowMsg { + denom: "tst".to_string(), + budget_for_all: Uint128::new(100), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: Timestamp::default(), + }; + let res = execute_register_bonus_window( + deps.as_mut(), + mock_env(), + info, + msg, + ).unwrap(); + + assert_eq!(0, res.messages.len()); + + // meaninng test of the windows query + let bonus_windows = query_bonus_windows(deps.as_ref()).unwrap(); + assert_eq!(1, bonus_windows.len()); + + // meaning test of window query + let bonus_window = query_bonus_window(deps.as_ref(), 0).unwrap(); + assert_eq!(bonus_window.id, 0); + } + + // Success case to check id sequencing + { + let info = mock_info("authority", &coins(100, "tst")); + let msg = RegisterBonusWindowMsg { + denom: "tst".to_string(), + budget_for_all: Uint128::zero(), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: Timestamp::default(), + }; + let res = execute_register_bonus_window( + deps.as_mut(), + mock_env(), + info, + msg, + ).unwrap(); + + assert_eq!(0, res.messages.len()); + + let bonus_window = query_bonus_window(deps.as_ref(), 1).unwrap(); + assert_eq!(bonus_window.id, 1); + } + +} diff --git a/contracts/yieldaggregator-bonus/tests/stake_vault_share.rs b/contracts/yieldaggregator-bonus/tests/stake_vault_share.rs new file mode 100644 index 0000000..1927958 --- /dev/null +++ b/contracts/yieldaggregator-bonus/tests/stake_vault_share.rs @@ -0,0 +1,117 @@ +use crate::helpers::setup; +use cosmwasm_std::{testing::{mock_env, mock_info}, Uint128, Decimal, Timestamp, coins}; +use yieldaggregator_bonus::{ + error::ContractError, execute::{update_params::execute_update_params, register_bonus_window::execute_register_bonus_window, stake_vault_share::execute_stake_vault_share}, msgs::{UpdateParamsMsg, RegisterBonusWindowMsg, self, StakeVaultShareMsg}, + query::{params::query_params, bonus_windows::{query_bonus_windows, query_bonus_window}, vault_share_staking::query_vault_share_staking}, +}; + +mod helpers; + +#[test] +fn test_stake_vault_share() { + let mut deps = setup(); + // first, register the bonus window + let bw_denom = format!("yieldaggregator/vault/{}", "0"); + execute_register_bonus_window( + deps.as_mut(), + mock_env(), + mock_info("authority", &coins(100, bw_denom.clone())), + RegisterBonusWindowMsg{ + denom: bw_denom.clone(), + budget_for_all: Uint128::zero(), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: Timestamp::default().plus_seconds(1), + }, + ).unwrap(); + + // Error due to invalid denom + { + let info = mock_info( + "anyone", + &coins(100, "invalid_denom".to_string()) + ); + + let stake_msg = StakeVaultShareMsg { + bonus_window_id: 0, + vault_id: 0, + }; + let err = execute_stake_vault_share( + deps.as_mut(), + mock_env(), + info, + stake_msg + ).unwrap_err(); + + assert_eq!(ContractError::NoAllowedToken { }, err); + } + + // Error due to invalid time window + { + let info = mock_info("anyone", &coins(100, bw_denom.clone())); + let stake_msg = StakeVaultShareMsg { + bonus_window_id: 0, + vault_id: 0, + }; + let err = execute_stake_vault_share( + deps.as_mut(), + mock_env(), + info, + stake_msg + ).unwrap_err(); + + assert_eq!(ContractError::InvalidBonusWindowPeriod { }, err); + } + + // Success + { + let bw_denom = format!("yieldaggregator/vault/{}", "0"); + execute_register_bonus_window( + deps.as_mut(), + mock_env(), + mock_info("authority", &coins(100, bw_denom.clone())), + RegisterBonusWindowMsg{ + denom: bw_denom.clone(), + budget_for_all: Uint128::zero(), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: mock_env().block.time.plus_seconds(1000), + }, + ).unwrap(); + + let info = mock_info("anyone", &coins(100, bw_denom.clone())); + let stake_msg = StakeVaultShareMsg { + bonus_window_id: 1, + vault_id: 0, + }; + let res = execute_stake_vault_share( + deps.as_mut(), + mock_env(), + info.clone(), + stake_msg + ).unwrap(); + + assert_eq!(0, res.messages.len()); + + // meaning test of the vault share query + let vault_share = query_vault_share_staking(deps.as_ref(), 0, info.clone().sender.into_string()).unwrap(); + assert_eq!(vault_share.vault_share, Uint128::new(100)); + } + + // Error due to already staked + { + let info = mock_info("anyone", &coins(100, bw_denom.clone())); + let stake_msg = StakeVaultShareMsg { + bonus_window_id: 1, + vault_id: 0, + }; + let err = execute_stake_vault_share( + deps.as_mut(), + mock_env(), + info, + stake_msg + ).unwrap_err(); + + assert_eq!(err, ContractError::AlreadyStaked { }); + } +} diff --git a/contracts/yieldaggregator-bonus/tests/update_params.rs b/contracts/yieldaggregator-bonus/tests/update_params.rs index 8633fbd..2090775 100644 --- a/contracts/yieldaggregator-bonus/tests/update_params.rs +++ b/contracts/yieldaggregator-bonus/tests/update_params.rs @@ -27,11 +27,11 @@ fn test_update_params() { let params = query_params(deps.as_ref()).unwrap(); assert_eq!(params.authority.as_str(), "authority"); - let invalid_info = mock_info("authority", &[]); + let info = mock_info("authority", &[]); execute_update_params( deps.as_mut(), mock_env(), - invalid_info, + info, UpdateParamsMsg { authority: Some("authority2".to_string()), }, @@ -40,4 +40,19 @@ fn test_update_params() { let params = query_params(deps.as_ref()).unwrap(); assert_eq!(params.authority.as_str(), "authority2"); + + // // Success + // let info = mock_info("authority", &[]); + // execute_update_params( + // deps.as_mut(), + // mock_env(), + // invalid_info, + // UpdateParamsMsg { + // authority: Some("authority2".to_string()), + // }, + // ) + // .unwrap(); + + // let params = query_params(deps.as_ref()).unwrap(); + // assert_eq!(params.authority.as_str(), "authority2"); } diff --git a/contracts/yieldaggregator-bonus/tests/vote.rs b/contracts/yieldaggregator-bonus/tests/vote.rs new file mode 100644 index 0000000..54b37db --- /dev/null +++ b/contracts/yieldaggregator-bonus/tests/vote.rs @@ -0,0 +1,112 @@ +use crate::helpers::setup; +use cosmwasm_std::{testing::{mock_env, mock_info}, Uint128, Decimal, Timestamp, coins}; +use yieldaggregator_bonus::{ + error::ContractError, execute::{update_params::execute_update_params, register_bonus_window::execute_register_bonus_window, vote::execute_vote}, msgs::{UpdateParamsMsg, RegisterBonusWindowMsg, VoteMsg}, + query::{params::query_params, voted_vaults::query_voted_vaults}, +}; + +mod helpers; + +#[test] +fn test_vote() { + let mut deps = setup(); + + // First, register the bonus window + execute_register_bonus_window( + deps.as_mut(), + mock_env(), + mock_info("authority", &coins(100, "test")), + RegisterBonusWindowMsg{ + denom: "test".to_string(), + budget_for_all: Uint128::new(100), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: mock_env().block.time.plus_seconds(1000), + }, + ).unwrap(); + + // Error: because of the invalid token + { + let invalid_info = mock_info("anyone", &coins(100, "invalid")); + let err = execute_vote( + deps.as_mut(), + mock_env(), + invalid_info, + VoteMsg { + bonus_window_id: 0, + vault_id: 0, + }, + ).unwrap_err(); + + assert_eq!(err, ContractError::NoAllowedToken {}); + } + + // Error: because of the invalid bonus time window + { + execute_register_bonus_window( + deps.as_mut(), + mock_env(), + mock_info("authority", &coins(100, "test")), + RegisterBonusWindowMsg{ + denom: "test".to_string(), + budget_for_all: Uint128::new(100), + apr_for_winners: vec![Decimal::zero()], + start_at: Timestamp::default(), + end_at: Timestamp::default().plus_seconds(1) + }, + ).unwrap(); + + let info = mock_info("anyone", &coins(100, "test")); + let err = execute_vote( + deps.as_mut(), + mock_env(), + info, + VoteMsg { + bonus_window_id: 1, + vault_id: 0, + }, + ).unwrap_err(); + + assert_eq!(err, ContractError::InvalidBonusWindowPeriod { }); + } + + // Success for the first vote + { + let info = mock_info("anyone", &coins(100, "test")); + let res = execute_vote( + deps.as_mut(), + mock_env(), + info, + VoteMsg { + bonus_window_id: 0, + vault_id: 0, + }, + ).unwrap(); + + assert_eq!(0, res.messages.len()); + + let voted_vault = query_voted_vaults(deps.as_ref(), 0).unwrap(); + assert_eq!(voted_vault.len(), 1); + assert_eq!(voted_vault[0].voted_amount, Uint128::new(100)); + } + + // Success for the second vote + { + let info = mock_info("anyone", &coins(100, "test")); + let res = execute_vote( + deps.as_mut(), + mock_env(), + info, + VoteMsg { + bonus_window_id: 0, + vault_id: 0, + }, + ).unwrap(); + + assert_eq!(0, res.messages.len()); + + let voted_vault = query_voted_vaults(deps.as_ref(), 0).unwrap(); + assert_eq!(voted_vault.len(), 1); + assert_eq!(voted_vault[0].voted_amount, Uint128::new(200)); + } +}