From 0d03e5e1d9b1f8767a45742074eab834adb935c1 Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Fri, 2 Feb 2024 13:41:02 +0100 Subject: [PATCH 01/17] wip --- contracts/core-token-vesting/src/contract.rs | 386 +++++++++++++++++-- contracts/core-token-vesting/src/errors.rs | 1 + contracts/core-token-vesting/src/msg.rs | 76 ++++ contracts/core-token-vesting/src/state.rs | 22 +- 4 files changed, 462 insertions(+), 23 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index a73c965..b2a4add 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -1,9 +1,9 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_json, to_json_binary, Attribute, BankMsg, Binary, Coin, CosmosMsg, - Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, - Storage, Timestamp, Uint128, WasmMsg, + from_json, to_json_binary, Addr, Attribute, BankMsg, Binary, Coin, + CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, + StdResult, Storage, Timestamp, Uint128, WasmMsg, }; use serde_json::to_string; @@ -13,10 +13,13 @@ use cw_storage_plus::Bound; use crate::errors::ContractError; use crate::msg::{ - Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, VestingAccountResponse, - VestingData, VestingSchedule, + Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, RewardUserRequest, + RewardUserResponse, VestingAccountResponse, VestingData, VestingSchedule, +}; +use crate::state::{ + denom_to_key, Campaign, DeregisterResult, VestingAccount, CAMPAIGN, + USER_REWARDS, VESTING_ACCOUNTS, }; -use crate::state::{denom_to_key, VestingAccount, VESTING_ACCOUNTS}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -68,19 +71,226 @@ pub fn execute( denom, vested_token_recipient, left_vesting_token_recipient, - } => deregister_vesting_account( + } => { + let response = deregister_vesting_account( + deps, + &env, + &info, + address, + &denom, + vested_token_recipient, + left_vesting_token_recipient, + ); + + if response.is_err() { + Err(response.err().unwrap().into()) + } else { + let result = response.unwrap(); + Ok(Response::new() + .add_messages(result.msgs) + .add_attributes(result.attributes)) + } + } + ExecuteMsg::Claim { denoms, recipient } => { + claim(deps, env, info, denoms, recipient) + } + ExecuteMsg::CreateCampaign { + vesting_schedule, + campaign_id, + campaign_name, + campaign_description, + managers, + } => create_campaign( deps, env, info, - address, + vesting_schedule, + campaign_id, + campaign_name, + campaign_description, + managers, + ), + ExecuteMsg::RewardUsers { + campaign_id, + requests, + } => reward_users(deps, env, info, campaign_id, requests), + ExecuteMsg::ClaimCampaign { campaign_id } => { + claim_campaign(deps, env, info, campaign_id) + } + ExecuteMsg::DeregisterVestingAccounts { + addresses, denom, vested_token_recipient, left_vesting_token_recipient, + } => deregister_vesting_accounts( + deps, + env, + info, + addresses, + &denom, + vested_token_recipient, + left_vesting_token_recipient, ), - ExecuteMsg::Claim { denoms, recipient } => { - claim(deps, env, info, denoms, recipient) + ExecuteMsg::DeactivateCampaign { campaign_id } => { + deactivate_campaign(deps, env, info, campaign_id) } + ExecuteMsg::Withdraw { + amount, + campaign_id, + } => withdraw(deps, env, info, amount, campaign_id), + } +} + +fn deactivate_campaign( + deps: DepsMut, + env: Env, + info: MessageInfo, + campaign_id: String, +) -> Result { + let mut campaign = CAMPAIGN + .load(deps.storage, campaign_id.clone()) + .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; + + if campaign.owner != info.sender + && !campaign.managers.contains(&info.sender.to_string()) + { + return Err(StdError::generic_err("unauthorized").into()); + } + + if !campaign.is_active { + return Ok(Response::new() + .add_attribute("method", "deactivate") + .add_attribute("message", "Campaign is already deactivated")); + } + + campaign.is_active = false; + CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; + + let bond_denom = deps.querier.query_bonded_denom()?; + let own_balance: Uint128 = deps + .querier + .query_balance(&env.contract.address, bond_denom.clone()) + .map_err(|_| StdError::generic_err("Failed to query contract balance"))? + .amount; + + return withdraw(deps, env, info, own_balance, campaign_id); +} + +fn claim_campaign( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _campaign_id: String, +) -> Result { + todo!() +} + +fn reward_users( + deps: DepsMut, + env: Env, + info: MessageInfo, + campaign_id: String, + requests: Vec, +) -> Result { + let mut res = vec![]; + + let mut campaign = CAMPAIGN + .load(deps.storage, campaign_id.clone()) + .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; + + if campaign.owner != info.sender + && !campaign.managers.contains(&info.sender.into_string()) + { + return Err(StdError::generic_err("Unauthorized").into()); + } + + if !campaign.is_active { + return Err(StdError::generic_err("Campaign is not active").into()); + } + + for req in requests { + if campaign.unallocated_amount < req.amount { + return Err(StdError::generic_err( + "Not enough funds in the campaign", + ) + .into()); + } + + match USER_REWARDS.may_load(deps.storage, req.user_address.clone())? { + Some(mut user_reward) => { + user_reward += req.amount; + USER_REWARDS.save( + deps.storage, + req.user_address.clone(), + &user_reward, + )?; + } + None => { + USER_REWARDS.save( + deps.storage, + req.user_address.clone(), + &req.amount, + )?; + } + }; + campaign.unallocated_amount -= req.amount; + CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; + + res.push(RewardUserResponse { + user_address: req.user_address.clone(), + success: true, + error_msg: "".to_string(), + }); + } + + Ok(Response::new() + .add_attribute("method", "reward_users") + .set_data(to_json_binary(&res).unwrap())) +} + +fn create_campaign( + deps: DepsMut, + env: Env, + info: MessageInfo, + vesting_schedule: VestingSchedule, + campaign_id: String, + campaign_name: String, + campaign_description: String, + managers: Vec, +) -> Result { + if CAMPAIGN + .may_load(deps.storage, campaign_id.clone())? + .is_some() + { + return Err(StdError::generic_err("Campaign already exists").into()); + } + + if info.funds.len() != 1 { + return Err(StdError::generic_err("Only one coin is allowed").into()); } + + let bond_denom = deps.querier.query_bonded_denom()?; + let coin = info.funds.get(0).unwrap(); + if coin.denom != bond_denom { + return Err( + StdError::generic_err("Only native tokens are allowed").into() + ); + } + + let campaign = Campaign { + campaign_name: campaign_name, + campaign_id: campaign_id.clone(), + campaign_description: campaign_description, + owner: info.sender.into_string(), + managers: managers, + unallocated_amount: coin.amount, + is_active: true, + }; + CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; + + Ok(Response::new() + .add_attribute("method", "create_campaign") + .add_attribute("campaign_id", campaign_id)) } fn register_vesting_account( @@ -127,17 +337,56 @@ fn register_vesting_account( ])) } -fn deregister_vesting_account( +fn deregister_vesting_accounts( deps: DepsMut, env: Env, info: MessageInfo, - address: String, - denom: Denom, + addresses: Vec, + denom: &Denom, vested_token_recipient: Option, left_vesting_token_recipient: Option, ) -> Result { + let mut messages: Vec = vec![]; + let mut attrs: Vec<(&str, String)> = vec![]; + + for address in addresses.iter() { + let response = deregister_vesting_account( + deps.clone(), + &env, + &info, + address.clone(), + denom, + vested_token_recipient.clone(), + left_vesting_token_recipient.clone(), + ); + + if response.is_err() { + let error_message = response.err().unwrap().to_string(); + attrs.extend(vec![ + ("action", "deregister_vesting_accounts".to_string()), + ("address", address.to_string()), + ("error", error_message), + ]); + } else { + let result = response.unwrap(); + messages.extend(result.msgs); + attrs.extend(result.attributes); + } + } + Ok(Response::new().add_messages(messages).add_attributes(attrs)) +} + +fn deregister_vesting_account<'a>( + deps: DepsMut, + env: &Env, + info: &MessageInfo, + address: String, + denom: &Denom, + vested_token_recipient: Option, + left_vesting_token_recipient: Option, +) -> Result, ContractError> { let denom_key = denom_to_key(denom.clone()); - let sender = info.sender; + let sender = info.sender.clone(); let mut messages: Vec = vec![]; @@ -195,14 +444,19 @@ fn deregister_vesting_account( messages.push(msg_send); } - Ok(Response::new().add_messages(messages).add_attributes(vec![ - ("action", "deregister_vesting_account"), - ("address", address.as_str()), - ("vesting_denom", &to_string(&account.vesting_denom).unwrap()), - ("vesting_amount", &account.vesting_amount.to_string()), - ("vested_amount", &vested_amount.to_string()), - ("left_vesting_amount", &left_vesting_amount.to_string()), - ])) + let result = DeregisterResult { + msgs: messages, + attributes: vec![ + ("action", "deregister_vesting_account".to_string()), + ("address", address), + ("vesting_denom", to_string(&account.vesting_denom).unwrap()), + ("vesting_amount", account.vesting_amount.to_string()), + ("vested_amount", vested_amount.to_string()), + ("left_vesting_amount", left_vesting_amount.to_string()), + ], + }; + + Ok(result) } fn claim( @@ -397,6 +651,94 @@ fn vesting_account( Ok(VestingAccountResponse { address, vestings }) } +/// Allow the contract owner to withdraw native tokens +/// +/// Ensures the requested amount is available in the contract balance. Transfers +/// tokens to the contract owner's account. +pub fn withdraw( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + campaign_id: String, +) -> Result { + let campaign = CAMPAIGN.load(deps.storage, campaign_id)?; + + if info.sender != campaign.owner { + return Err( + StdError::generic_err("Only contract owner can withdraw").into() + ); + } + + let bond_denom = deps.querier.query_bonded_denom()?; + + let own_balance: Uint128 = deps + .querier + .query_balance(env.contract.address, bond_denom.clone()) + .map_err(|_| { + ContractError::Std(StdError::generic_err( + "Failed to query contract balance", + )) + })? + .amount; + + if amount > own_balance { + return Err( + StdError::generic_err("Not enough funds in the contract").into() + ); + } + + let res = Response::new() + .add_attribute("method", "withdraw") + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom: bond_denom.clone(), + amount, + }], + })); + + // Update campaign unallocated amount + if amount > campaign.unallocated_amount { + let update_result = CAMPAIGN.update( + deps.storage, + campaign_id, + |mut campaign| -> StdResult { + if let Some(mut campaign) = campaign { + campaign.unallocated_amount = Uint128::zero(); + Ok(campaign) + } else { + Err(StdError::generic_err("Campaign not found")) + } + }, + ); + + if let Err(e) = update_result { + return Err(e.into()); + } + } else { + let update_result = CAMPAIGN.update( + deps.storage, + campaign_id, + |mut campaign| -> StdResult { + if let Some(mut campaign) = campaign { + campaign.unallocated_amount -= amount; + Ok(campaign) + } else { + Err(StdError::generic_err("Campaign not found")) + } + }, + ); + + if let Err(e) = update_result { + return Err(e.into()); + } + } + Err(StdError::generic_err("Campaign not found"))?; + + return Ok(res); +} + #[cfg(test)] pub mod tests { diff --git a/contracts/core-token-vesting/src/errors.rs b/contracts/core-token-vesting/src/errors.rs index be9b992..336a827 100644 --- a/contracts/core-token-vesting/src/errors.rs +++ b/contracts/core-token-vesting/src/errors.rs @@ -13,6 +13,7 @@ pub enum ContractError { #[error(transparent)] Overflow(#[from] cosmwasm_std::OverflowError), + } #[derive(thiserror::Error, Debug, PartialEq)] diff --git a/contracts/core-token-vesting/src/msg.rs b/contracts/core-token-vesting/src/msg.rs index ca496fb..e10dff1 100644 --- a/contracts/core-token-vesting/src/msg.rs +++ b/contracts/core-token-vesting/src/msg.rs @@ -46,6 +46,82 @@ pub enum ExecuteMsg { denoms: Vec, recipient: Option, }, + + /// Create campaign to reward users with vested tokens + /// Args: + /// - vesting_schedule: VestingSchedule: The vesting schedule of the account. + /// - campaign_id: String: The unique identifier of the campaign. + /// - campaign_name: String: The name of the campaign. + /// - campaign_description: String: The description of the campaign. + /// - managers: Vec: The list of addresses that can manage the campaign (reward users). + CreateCampaign { + vesting_schedule: VestingSchedule, + + campaign_id: String, + campaign_name: String, + campaign_description: String, + managers: Vec, + }, + + /// Reward users with tokens + /// Args: + /// - campaign_id: String: The unique identifier of the campaign. + /// - requests: Vec: The list of reward requests. + RewardUsers { + campaign_id: String, + requests: Vec, + }, + + /// Deregister vesting accounts + /// Args: + /// - address: Vec: The list of addresses of the vesting accounts to be deregistered. + /// - denoms: Vec: The list of denoms of the vesting accounts to be deregistered. + /// - vested_token_recipient: Option: Bech 32 address that will receive the vested + /// tokens after deregistration. If None, tokens are received by the owner address. + DeregisterVestingAccounts { + addresses: Vec, + denom: Denom, + vested_token_recipient: Option, + left_vesting_token_recipient: Option, + }, + + /// Claim campaign: A user can claim vested tokens from a campaign and this + /// will register a vesting account for the user. + /// Args: + /// - campaign_id: String: The unique identifier of the campaign. + ClaimCampaign { + campaign_id: String, + }, + + /// Deactivate campaign: The campaign owner can deactivate the campaign. + /// All the unallocated tokens will be returned to the owner. + /// Args: + /// - campaign_id: String: The unique identifier of the campaign. + DeactivateCampaign { + campaign_id: String, + }, + + /// Withdraw: The campaign owner can withdraw unallocated tokens from the campaign. + /// Args: + /// - campaign_id: String: The unique identifier of the campaign. + /// - amount: Uint128: The amount of tokens to be withdrawn. + Withdraw { + campaign_id: String, + amount: Uint128, + }, +} + +#[cw_serde] +pub struct RewardUserRequest { + pub user_address: String, + pub amount: Uint128, +} + +#[cw_serde] +pub struct RewardUserResponse { + pub user_address: String, + pub success: bool, + pub error_msg: String, } #[cw_serde] diff --git a/contracts/core-token-vesting/src/state.rs b/contracts/core-token-vesting/src/state.rs index 729586a..9eddd58 100644 --- a/contracts/core-token-vesting/src/state.rs +++ b/contracts/core-token-vesting/src/state.rs @@ -1,10 +1,12 @@ use cosmwasm_schema::cw_serde; use crate::msg::VestingSchedule; -use cosmwasm_std::Uint128; +use cosmwasm_std::{CosmosMsg, Uint128}; use cw20::Denom; use cw_storage_plus::Map; +pub const CAMPAIGN: Map = Map::new("campaign"); +pub const USER_REWARDS: Map = Map::new("user_rewards"); pub const VESTING_ACCOUNTS: Map<(&str, &str), VestingAccount> = Map::new("vesting_accounts"); @@ -18,6 +20,24 @@ pub struct VestingAccount { pub claimed_amount: Uint128, } +#[cw_serde] +pub struct Campaign { + pub campaign_id: String, + pub campaign_name: String, + pub campaign_description: String, + + pub unallocated_amount: Uint128, + pub owner: String, + pub managers: Vec, + + pub is_active: bool, +} + +pub struct DeregisterResult<'a> { + pub msgs: Vec, + pub attributes: Vec<(&'a str, String)>, +} + pub fn denom_to_key(denom: Denom) -> String { match denom { Denom::Cw20(addr) => format!("cw20-{}", addr), From df268a5755496e1c2bea56d02af34828e8f5fe15 Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Fri, 2 Feb 2024 16:36:02 +0100 Subject: [PATCH 02/17] fix: fix issues with deregister --- Cargo.lock | 67 --------- contracts/core-token-vesting/src/contract.rs | 143 +++++-------------- contracts/core-token-vesting/src/msg.rs | 13 -- 3 files changed, 37 insertions(+), 186 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49b5e3e..29b4fb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1050,21 +1050,6 @@ dependencies = [ "memmap2 0.5.10", ] -[[package]] -name = "e2e-tests" -version = "0.1.0" -dependencies = [ - "anyhow", - "bash-rs", - "cosmwasm-schema", - "cosmwasm-std", - "home", - "serde", - "serde_json", - "thiserror", - "toml", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -2493,15 +2478,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2810,40 +2786,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" -dependencies = [ - "indexmap 2.1.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tower-service" version = "0.3.2" @@ -3459,15 +3401,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" -[[package]] -name = "winnow" -version = "0.5.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.50.0" diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index b2a4add..24d2ef4 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -1,9 +1,9 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_json, to_json_binary, Addr, Attribute, BankMsg, Binary, Coin, - CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, - StdResult, Storage, Timestamp, Uint128, WasmMsg, + from_json, to_json_binary, Attribute, BankMsg, Binary, Coin, CosmosMsg, + Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, + Storage, Timestamp, Uint128, WasmMsg, }; use serde_json::to_string; @@ -17,8 +17,8 @@ use crate::msg::{ RewardUserResponse, VestingAccountResponse, VestingData, VestingSchedule, }; use crate::state::{ - denom_to_key, Campaign, DeregisterResult, VestingAccount, CAMPAIGN, - USER_REWARDS, VESTING_ACCOUNTS, + denom_to_key, Campaign, VestingAccount, CAMPAIGN, USER_REWARDS, + VESTING_ACCOUNTS, }; #[cfg_attr(not(feature = "library"), entry_point)] @@ -71,26 +71,15 @@ pub fn execute( denom, vested_token_recipient, left_vesting_token_recipient, - } => { - let response = deregister_vesting_account( - deps, - &env, - &info, - address, - &denom, - vested_token_recipient, - left_vesting_token_recipient, - ); - - if response.is_err() { - Err(response.err().unwrap().into()) - } else { - let result = response.unwrap(); - Ok(Response::new() - .add_messages(result.msgs) - .add_attributes(result.attributes)) - } - } + } => deregister_vesting_account( + deps, + env, + info, + address, + denom, + vested_token_recipient, + left_vesting_token_recipient, + ), ExecuteMsg::Claim { denoms, recipient } => { claim(deps, env, info, denoms, recipient) } @@ -117,27 +106,13 @@ pub fn execute( ExecuteMsg::ClaimCampaign { campaign_id } => { claim_campaign(deps, env, info, campaign_id) } - ExecuteMsg::DeregisterVestingAccounts { - addresses, - denom, - vested_token_recipient, - left_vesting_token_recipient, - } => deregister_vesting_accounts( - deps, - env, - info, - addresses, - &denom, - vested_token_recipient, - left_vesting_token_recipient, - ), ExecuteMsg::DeactivateCampaign { campaign_id } => { deactivate_campaign(deps, env, info, campaign_id) } ExecuteMsg::Withdraw { amount, campaign_id, - } => withdraw(deps, env, info, amount, campaign_id), + } => withdraw(deps, env, info, amount, campaign_id.as_str()), } } @@ -173,7 +148,7 @@ fn deactivate_campaign( .map_err(|_| StdError::generic_err("Failed to query contract balance"))? .amount; - return withdraw(deps, env, info, own_balance, campaign_id); + return withdraw(deps, env, info, own_balance, campaign_id.as_str()); } fn claim_campaign( @@ -187,7 +162,7 @@ fn claim_campaign( fn reward_users( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, campaign_id: String, requests: Vec, @@ -250,9 +225,9 @@ fn reward_users( fn create_campaign( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, - vesting_schedule: VestingSchedule, + _vesting_schedule: VestingSchedule, campaign_id: String, campaign_name: String, campaign_description: String, @@ -337,56 +312,17 @@ fn register_vesting_account( ])) } -fn deregister_vesting_accounts( +fn deregister_vesting_account( deps: DepsMut, env: Env, info: MessageInfo, - addresses: Vec, - denom: &Denom, - vested_token_recipient: Option, - left_vesting_token_recipient: Option, -) -> Result { - let mut messages: Vec = vec![]; - let mut attrs: Vec<(&str, String)> = vec![]; - - for address in addresses.iter() { - let response = deregister_vesting_account( - deps.clone(), - &env, - &info, - address.clone(), - denom, - vested_token_recipient.clone(), - left_vesting_token_recipient.clone(), - ); - - if response.is_err() { - let error_message = response.err().unwrap().to_string(); - attrs.extend(vec![ - ("action", "deregister_vesting_accounts".to_string()), - ("address", address.to_string()), - ("error", error_message), - ]); - } else { - let result = response.unwrap(); - messages.extend(result.msgs); - attrs.extend(result.attributes); - } - } - Ok(Response::new().add_messages(messages).add_attributes(attrs)) -} - -fn deregister_vesting_account<'a>( - deps: DepsMut, - env: &Env, - info: &MessageInfo, address: String, - denom: &Denom, + denom: Denom, vested_token_recipient: Option, left_vesting_token_recipient: Option, -) -> Result, ContractError> { +) -> Result { let denom_key = denom_to_key(denom.clone()); - let sender = info.sender.clone(); + let sender = info.sender; let mut messages: Vec = vec![]; @@ -444,19 +380,14 @@ fn deregister_vesting_account<'a>( messages.push(msg_send); } - let result = DeregisterResult { - msgs: messages, - attributes: vec![ - ("action", "deregister_vesting_account".to_string()), - ("address", address), - ("vesting_denom", to_string(&account.vesting_denom).unwrap()), - ("vesting_amount", account.vesting_amount.to_string()), - ("vested_amount", vested_amount.to_string()), - ("left_vesting_amount", left_vesting_amount.to_string()), - ], - }; - - Ok(result) + Ok(Response::new().add_messages(messages).add_attributes(vec![ + ("action", "deregister_vesting_account"), + ("address", address.as_str()), + ("vesting_denom", &to_string(&account.vesting_denom).unwrap()), + ("vesting_amount", &account.vesting_amount.to_string()), + ("vested_amount", &vested_amount.to_string()), + ("left_vesting_amount", &left_vesting_amount.to_string()), + ])) } fn claim( @@ -660,9 +591,9 @@ pub fn withdraw( env: Env, info: MessageInfo, amount: Uint128, - campaign_id: String, + campaign_id: &str, ) -> Result { - let campaign = CAMPAIGN.load(deps.storage, campaign_id)?; + let campaign = CAMPAIGN.load(deps.storage, campaign_id.to_string())?; if info.sender != campaign.owner { return Err( @@ -702,8 +633,8 @@ pub fn withdraw( if amount > campaign.unallocated_amount { let update_result = CAMPAIGN.update( deps.storage, - campaign_id, - |mut campaign| -> StdResult { + campaign_id.to_string(), + |campaign| -> StdResult { if let Some(mut campaign) = campaign { campaign.unallocated_amount = Uint128::zero(); Ok(campaign) @@ -719,8 +650,8 @@ pub fn withdraw( } else { let update_result = CAMPAIGN.update( deps.storage, - campaign_id, - |mut campaign| -> StdResult { + campaign_id.to_string(), + |campaign| -> StdResult { if let Some(mut campaign) = campaign { campaign.unallocated_amount -= amount; Ok(campaign) diff --git a/contracts/core-token-vesting/src/msg.rs b/contracts/core-token-vesting/src/msg.rs index e10dff1..efc4df3 100644 --- a/contracts/core-token-vesting/src/msg.rs +++ b/contracts/core-token-vesting/src/msg.rs @@ -72,19 +72,6 @@ pub enum ExecuteMsg { requests: Vec, }, - /// Deregister vesting accounts - /// Args: - /// - address: Vec: The list of addresses of the vesting accounts to be deregistered. - /// - denoms: Vec: The list of denoms of the vesting accounts to be deregistered. - /// - vested_token_recipient: Option: Bech 32 address that will receive the vested - /// tokens after deregistration. If None, tokens are received by the owner address. - DeregisterVestingAccounts { - addresses: Vec, - denom: Denom, - vested_token_recipient: Option, - left_vesting_token_recipient: Option, - }, - /// Claim campaign: A user can claim vested tokens from a campaign and this /// will register a vesting account for the user. /// Args: From 33de27ed1c1d09fcd86e1e78ca940c6cfd68bd02 Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Fri, 2 Feb 2024 16:54:43 +0100 Subject: [PATCH 03/17] fix: fix several issues --- contracts/core-token-vesting/src/contract.rs | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 24d2ef4..9569d8d 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -116,6 +116,9 @@ pub fn execute( } } +/// Deactivate a campaign and withdraw all unallocated funds +/// This will also withdraw all unallocated funds from the contract +/// and send them to the campaign owner. fn deactivate_campaign( deps: DepsMut, env: Env, @@ -129,7 +132,7 @@ fn deactivate_campaign( if campaign.owner != info.sender && !campaign.managers.contains(&info.sender.to_string()) { - return Err(StdError::generic_err("unauthorized").into()); + return Err(StdError::generic_err("Unauthorized. Only the campaign owner or managers can deactivate the campaign").into()); } if !campaign.is_active { @@ -174,7 +177,7 @@ fn reward_users( .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; if campaign.owner != info.sender - && !campaign.managers.contains(&info.sender.into_string()) + && !campaign.managers.contains(&info.sender.into()) { return Err(StdError::generic_err("Unauthorized").into()); } @@ -183,8 +186,13 @@ fn reward_users( return Err(StdError::generic_err("Campaign is not active").into()); } + let mut unallocated_amount = campaign.unallocated_amount; + for req in requests { - if campaign.unallocated_amount < req.amount { + if unallocated_amount < req.amount { + // We fail on the first request that cannot be fulfilled + // This is to ensure that we do not partially fulfill requests + // and leave the campaign in an inconsistent state. return Err(StdError::generic_err( "Not enough funds in the campaign", ) @@ -208,8 +216,7 @@ fn reward_users( )?; } }; - campaign.unallocated_amount -= req.amount; - CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; + unallocated_amount -= req.amount; res.push(RewardUserResponse { user_address: req.user_address.clone(), @@ -218,6 +225,9 @@ fn reward_users( }); } + campaign.unallocated_amount = unallocated_amount; + CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; + Ok(Response::new() .add_attribute("method", "reward_users") .set_data(to_json_binary(&res).unwrap())) @@ -265,7 +275,9 @@ fn create_campaign( Ok(Response::new() .add_attribute("method", "create_campaign") - .add_attribute("campaign_id", campaign_id)) + .add_attribute("campaign_id", campaign_id) + .add_attribute("campaign_name", &campaign.campaign_name) + .add_attribute("initial_unallocated_amount", &coin.amount.to_string())) } fn register_vesting_account( @@ -665,7 +677,6 @@ pub fn withdraw( return Err(e.into()); } } - Err(StdError::generic_err("Campaign not found"))?; return Ok(res); } From 363dce55f3ebb3c917599137dfc341bc3c5da864 Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Tue, 6 Feb 2024 16:00:14 +0100 Subject: [PATCH 04/17] fix: move tests and add more tests --- contracts/core-token-vesting/Cargo.toml | 20 +- contracts/core-token-vesting/src/contract.rs | 156 ----- contracts/core-token-vesting/src/lib.rs | 3 - contracts/core-token-vesting/src/msg.rs | 41 -- .../core-token-vesting/tests/all_test.rs | 1 + .../tests/tests/helpers/helpers.rs | 1 + .../tests/tests/helpers/mod.rs | 2 + .../core-token-vesting/tests/tests/mod.rs | 5 + .../tests/tests/test_airdrop.rs | 628 ++++++++++++++++++ .../tests/tests/test_manager.rs | 149 +++++ .../tests/test_vesting.rs} | 49 +- 11 files changed, 838 insertions(+), 217 deletions(-) create mode 100644 contracts/core-token-vesting/tests/all_test.rs create mode 100644 contracts/core-token-vesting/tests/tests/helpers/helpers.rs create mode 100644 contracts/core-token-vesting/tests/tests/helpers/mod.rs create mode 100644 contracts/core-token-vesting/tests/tests/mod.rs create mode 100644 contracts/core-token-vesting/tests/tests/test_airdrop.rs create mode 100644 contracts/core-token-vesting/tests/tests/test_manager.rs rename contracts/core-token-vesting/{src/testing.rs => tests/tests/test_vesting.rs} (94%) diff --git a/contracts/core-token-vesting/Cargo.toml b/contracts/core-token-vesting/Cargo.toml index 7a2d3bf..7e71582 100644 --- a/contracts/core-token-vesting/Cargo.toml +++ b/contracts/core-token-vesting/Cargo.toml @@ -21,15 +21,15 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-schema = "1.4.0" -cosmwasm-std = "1.4.0" -cw20 = "1.1.1" -cw-utils = { version = "1.0.2" } -thiserror = { version = "1.0.49" } -cw-storage-plus = "1.1.0" -schemars = "0.8.15" -serde = { version = "1.0.188", default-features = false, features = ["derive"] } -serde_json = { version = "1.0", default-features = false, features = ["alloc"] } +cw20 = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-utils = { workspace = true } +thiserror = { workspace = true } +cw-storage-plus = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } \ No newline at end of file +anyhow = { workspace = true } diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 9569d8d..8003076 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -254,13 +254,7 @@ fn create_campaign( return Err(StdError::generic_err("Only one coin is allowed").into()); } - let bond_denom = deps.querier.query_bonded_denom()?; let coin = info.funds.get(0).unwrap(); - if coin.denom != bond_denom { - return Err( - StdError::generic_err("Only native tokens are allowed").into() - ); - } let campaign = Campaign { campaign_name: campaign_name, @@ -680,153 +674,3 @@ pub fn withdraw( return Ok(res); } - -#[cfg(test)] -pub mod tests { - - use super::*; - use anyhow::anyhow; - use cosmwasm_std::{ - coin, - testing::{self, MockApi, MockQuerier, MockStorage}, - Empty, OwnedDeps, Uint64, - }; - - pub type TestResult = Result<(), anyhow::Error>; - - pub fn mock_env_with_time(block_time: u64) -> Env { - let mut env = testing::mock_env(); - env.block.time = Timestamp::from_seconds(block_time); - env - } - - /// Convenience function for instantiating the contract at and setting up - /// the env to have the given block time. - pub fn setup_with_block_time( - block_time: u64, - ) -> anyhow::Result<(OwnedDeps, Env)> - { - let mut deps = testing::mock_dependencies(); - let env = mock_env_with_time(block_time); - instantiate( - deps.as_mut(), - env.clone(), - testing::mock_info("admin-sender", &[]), - InstantiateMsg {}, - )?; - Ok((deps, env)) - } - - #[test] - fn deregister_err_nonexistent_vesting_account() -> TestResult { - let (mut deps, _env) = setup_with_block_time(0)?; - - let msg = ExecuteMsg::DeregisterVestingAccount { - address: "nonexistent".to_string(), - denom: Denom::Native("token".to_string()), - vested_token_recipient: None, - left_vesting_token_recipient: None, - }; - - let res = execute( - deps.as_mut(), - testing::mock_env(), - testing::mock_info("admin-sender", &[]), - msg, - ); - - match res { - Ok(_) => Err(anyhow!("Unexpected result: {:#?}", res)), - Err(ContractError::Std(StdError::GenericErr { msg, .. })) => { - assert!(msg.contains("vesting entry is not found for denom")); - Ok(()) - } - Err(err) => Err(anyhow!("Unexpected error: {:#?}", err)), - } - } - - #[test] - fn deregister_err_unauthorized_vesting_account() -> TestResult { - // Set up the environment with a block time before the vesting start time - let (mut deps, env) = setup_with_block_time(50)?; - - let register_msg = ExecuteMsg::RegisterVestingAccount { - master_address: Some("addr0002".to_string()), - address: "addr0001".to_string(), - vesting_schedule: VestingSchedule::LinearVesting { - start_time: Uint64::new(100), - end_time: Uint64::new(110), - vesting_amount: Uint128::new(1000000u128), - }, - }; - - execute( - deps.as_mut(), - env.clone(), // Use the custom environment with the adjusted block time - testing::mock_info("admin-sender", &[coin(1000000, "token")]), - register_msg, - )?; - - // Try to deregister with unauthorized sender - let msg = ExecuteMsg::DeregisterVestingAccount { - address: "addr0001".to_string(), - denom: Denom::Native("token".to_string()), - vested_token_recipient: None, - left_vesting_token_recipient: None, - }; - - let res = execute( - deps.as_mut(), - env, // Use the custom environment with the adjusted block time - testing::mock_info("addr0003", &[]), - msg, - ); - match res { - Err(ContractError::Std(StdError::GenericErr { msg, .. })) - if msg == "unauthorized" => {} - _ => return Err(anyhow!("Unexpected result: {:?}", res)), - } - - Ok(()) - } - - #[test] - fn deregister_successful() -> TestResult { - // Set up the environment with a block time before the vesting start time - let (mut deps, env) = setup_with_block_time(50)?; - - let register_msg = ExecuteMsg::RegisterVestingAccount { - master_address: Some("addr0002".to_string()), - address: "addr0001".to_string(), - vesting_schedule: VestingSchedule::LinearVesting { - start_time: Uint64::new(100), - end_time: Uint64::new(110), - vesting_amount: Uint128::new(1000000u128), - }, - }; - - execute( - deps.as_mut(), - env.clone(), // Use the custom environment with the adjusted block time - testing::mock_info("admin-sender", &[coin(1000000, "token")]), - register_msg, - )?; - - // Deregister with the master address - let msg = ExecuteMsg::DeregisterVestingAccount { - address: "addr0001".to_string(), - denom: Denom::Native("token".to_string()), - vested_token_recipient: None, - left_vesting_token_recipient: None, - }; - - let _res = execute( - deps.as_mut(), - env, // Use the custom environment with the adjusted block time - testing::mock_info("addr0002", &[]), - msg, - )?; - - Ok(()) - } -} diff --git a/contracts/core-token-vesting/src/lib.rs b/contracts/core-token-vesting/src/lib.rs index 4368eaf..09d07c7 100644 --- a/contracts/core-token-vesting/src/lib.rs +++ b/contracts/core-token-vesting/src/lib.rs @@ -2,6 +2,3 @@ pub mod contract; pub mod errors; pub mod msg; pub mod state; - -#[cfg(test)] -mod testing; diff --git a/contracts/core-token-vesting/src/msg.rs b/contracts/core-token-vesting/src/msg.rs index efc4df3..cd5dab2 100644 --- a/contracts/core-token-vesting/src/msg.rs +++ b/contracts/core-token-vesting/src/msg.rs @@ -321,44 +321,3 @@ impl VestingSchedule { } } } - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::contract::tests::TestResult; - - #[test] - fn linear_vesting_vested_amount() -> TestResult { - let schedule = VestingSchedule::LinearVesting { - start_time: Uint64::new(100), - end_time: Uint64::new(110), - vesting_amount: Uint128::new(1000000u128), - }; - - assert_eq!(schedule.vested_amount(100)?, Uint128::zero()); - assert_eq!(schedule.vested_amount(105)?, Uint128::new(500000u128)); - assert_eq!(schedule.vested_amount(110)?, Uint128::new(1000000u128)); - assert_eq!(schedule.vested_amount(115)?, Uint128::new(1000000u128)); - - Ok(()) - } - - #[test] - fn linear_vesting_with_cliff_vested_amount() -> TestResult { - let schedule = VestingSchedule::LinearVestingWithCliff { - start_time: Uint64::new(100), - end_time: Uint64::new(110), - vesting_amount: Uint128::new(1_000_000_u128), - cliff_amount: Uint128::new(100_000_u128), - cliff_time: Uint64::new(105), - }; - - assert_eq!(schedule.vested_amount(100)?, Uint128::zero()); - assert_eq!(schedule.vested_amount(105)?, Uint128::new(100000u128)); // cliff time then the cliff amount - assert_eq!(schedule.vested_amount(120)?, Uint128::new(1000000u128)); // complete vesting - assert_eq!(schedule.vested_amount(104)?, Uint128::zero()); // before cliff time - assert_eq!(schedule.vested_amount(109)?, Uint128::new(820_000)); // after cliff time but before end time - - Ok(()) - } -} diff --git a/contracts/core-token-vesting/tests/all_test.rs b/contracts/core-token-vesting/tests/all_test.rs new file mode 100644 index 0000000..14f0038 --- /dev/null +++ b/contracts/core-token-vesting/tests/all_test.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/core-token-vesting/tests/tests/helpers/helpers.rs b/contracts/core-token-vesting/tests/tests/helpers/helpers.rs new file mode 100644 index 0000000..cc1097c --- /dev/null +++ b/contracts/core-token-vesting/tests/tests/helpers/helpers.rs @@ -0,0 +1 @@ +pub type TestResult = Result<(), anyhow::Error>; diff --git a/contracts/core-token-vesting/tests/tests/helpers/mod.rs b/contracts/core-token-vesting/tests/tests/helpers/mod.rs new file mode 100644 index 0000000..20d09c7 --- /dev/null +++ b/contracts/core-token-vesting/tests/tests/helpers/mod.rs @@ -0,0 +1,2 @@ +pub use self::helpers::TestResult; +pub mod helpers; diff --git a/contracts/core-token-vesting/tests/tests/mod.rs b/contracts/core-token-vesting/tests/tests/mod.rs new file mode 100644 index 0000000..71366eb --- /dev/null +++ b/contracts/core-token-vesting/tests/tests/mod.rs @@ -0,0 +1,5 @@ +mod helpers; + +mod test_airdrop; +mod test_manager; +mod test_vesting; diff --git a/contracts/core-token-vesting/tests/tests/test_airdrop.rs b/contracts/core-token-vesting/tests/tests/test_airdrop.rs new file mode 100644 index 0000000..c2d4395 --- /dev/null +++ b/contracts/core-token-vesting/tests/tests/test_airdrop.rs @@ -0,0 +1,628 @@ +use anyhow::anyhow; +use cosmwasm_std::{ + coin, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Empty, Env, OwnedDeps, StdError, Uint128, Uint64, +}; +use cw20::Denom; +use token_vesting::{ + contract::execute, + errors::ContractError, + msg::{ExecuteMsg, RewardUserRequest, VestingSchedule}, + state::{denom_to_key, CAMPAIGN, USER_REWARDS, VESTING_ACCOUNTS}, +}; + +use super::{helpers::TestResult, test_manager::setup_with_block_time}; + +#[test] +fn execute_register_vesting_account_valid() -> TestResult { + let (mut deps, env) = setup_with_block_time(100)?; + + let register_msg = ExecuteMsg::RegisterVestingAccount { + master_address: Some("master".to_string()), + address: "addr_valid".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(1000), + }, + }; + + let res = execute( + deps.as_mut(), + env, + mock_info("sender", &[coin(1000, "token")]), + register_msg, + ); + + assert!( + res.is_ok(), + "Expected successful registration, got: {:?}", + res.err() + ); + + // Verify the vesting account is correctly registered in the contract's state + let vesting_account = VESTING_ACCOUNTS.load( + deps.as_ref().storage, + ( + "addr_valid", + &denom_to_key(Denom::Native("token".to_string())), + ), + )?; + assert_eq!( + vesting_account.vesting_amount, + Uint128::new(1000), + "Vesting amount mismatch" + ); + + Ok(()) +} + +#[test] +fn execute_register_vesting_account_duplicate() -> TestResult { + let (mut deps, env) = setup_with_block_time(100)?; + + let register_msg_first = ExecuteMsg::RegisterVestingAccount { + master_address: Some("master".to_string()), + address: "addr_duplicate".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(1000), + }, + }; + + // First registration should succeed + let _ = execute( + deps.as_mut(), + env.clone(), + mock_info("sender", &[coin(1000, "token")]), + register_msg_first.clone(), + )?; + + // Attempt to register again with the same address and token denomination + let register_msg_second = register_msg_first.clone(); + let res = execute( + deps.as_mut(), + env, + mock_info("sender", &[coin(1000, "token")]), + register_msg_second, + ); + + assert!( + matches!(res, Err(ContractError::Std(StdError::GenericErr { .. }))), + "Expected duplicate registration error, got: {:?}", + res + ); + + Ok(()) +} + +#[test] +fn execute_register_vesting_account_invalid_deposit() -> TestResult { + let (mut deps, env) = setup_with_block_time(100)?; + + // Attempt registration with invalid deposit details + let res = execute( + deps.as_mut(), + env, + mock_info("addr_sender", &[coin(500, "invalid_token")]), // Incorrect token denomination + ExecuteMsg::RegisterVestingAccount { + master_address: Some("addr_master".to_string()), + address: "addr_vesting".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(1000u128), + }, + }, + ); + + assert!( + matches!(res, Err(ContractError::Std(StdError::GenericErr { msg, .. })) if msg.contains("Only native tokens are allowed")) + ); + + Ok(()) +} + +#[test] +fn execute_deregister_vesting_account_authorized() -> TestResult { + let (mut deps, env) = setup_with_block_time(100)?; + + // Simulate registering a vesting account first + let vesting_account_address = "vesting_account"; + let master_address = "master_address"; + let register_msg = ExecuteMsg::RegisterVestingAccount { + master_address: Some(master_address.to_string()), + address: vesting_account_address.to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(50), + end_time: Uint64::new(150), + vesting_amount: Uint128::new(1000), + }, + }; + execute( + deps.as_mut(), + env.clone(), + mock_info("creator", &[coin(1000, "token")]), + register_msg, + )?; + + // Attempt to deregister the vesting account by the master address + let deregister_msg = ExecuteMsg::DeregisterVestingAccount { + address: vesting_account_address.to_string(), + denom: Denom::Native("token".to_string()), + vested_token_recipient: Some("recipient1".to_string()), + left_vesting_token_recipient: Some("recipient2".to_string()), + }; + let res = execute( + deps.as_mut(), + env, + mock_info(master_address, &[]), + deregister_msg, + )?; + + // Assertions to verify the expected state changes and messages + assert_eq!( + res.messages.len(), + 2, + "Expected two bank send messages for token transfer" + ); + assert!( + res.attributes.iter().any(|attr| attr.key == "action" + && attr.value == "deregister_vesting_account"), + "Expected 'deregister_vesting_account' action in response attributes" + ); + + Ok(()) +} + +#[test] +fn execute_deregister_vesting_account_unauthorized() -> TestResult { + let (mut deps, env) = setup_with_block_time(100)?; + + // Simulate registering a vesting account first + let vesting_account_address = "vesting_account"; + let master_address = "master_address"; + let register_msg = ExecuteMsg::RegisterVestingAccount { + master_address: Some(master_address.to_string()), + address: vesting_account_address.to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(50), + end_time: Uint64::new(150), + vesting_amount: Uint128::new(1000), + }, + }; + execute( + deps.as_mut(), + env.clone(), + mock_info("creator", &[coin(1000, "token")]), + register_msg, + )?; + + // Attempt to deregister the vesting account by an unauthorized user + let deregister_msg = ExecuteMsg::DeregisterVestingAccount { + address: vesting_account_address.to_string(), + denom: Denom::Native("token".to_string()), + vested_token_recipient: None, + left_vesting_token_recipient: None, + }; + let res = execute( + deps.as_mut(), + env, + mock_info("unauthorized_user", &[]), + deregister_msg, + ); + + assert!( + matches!(res, Err(ContractError::Std(StdError::GenericErr { msg, .. })) if msg.contains("unauthorized")), + "Expected an unauthorized error" + ); + + Ok(()) +} + +#[test] +fn execute_create_campaign_valid() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Create a campaign with valid parameters + let create_campaign_msg = ExecuteMsg::CreateCampaign { + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(5000), + }, + campaign_id: "campaign1".to_string(), + campaign_name: "Test Campaign".to_string(), + campaign_description: "A test campaign".to_string(), + managers: vec!["manager1".to_string(), "manager2".to_string()], + }; + let res = execute( + deps.as_mut(), + env, + mock_info("creator", &[coin(5000, "token")]), + create_campaign_msg, + )?; + + // Assertions to verify the campaign is created correctly + assert!( + res.attributes + .iter() + .any(|attr| attr.key == "method" && attr.value == "create_campaign"), + "Expected 'create_campaign' method in response attributes" + ); + assert!( + CAMPAIGN.has(&deps.storage, "campaign1".to_string()), + "Campaign should be saved in state" + ); + + Ok(()) +} + +#[test] +fn execute_create_campaign_duplicate_id() -> TestResult { + let (mut deps, _env) = setup_with_block_time(0)?; + + // Create a campaign with a unique ID + let campaign_id = "unique_campaign_id"; + let create_campaign_msg = ExecuteMsg::CreateCampaign { + campaign_id: campaign_id.to_string(), + campaign_name: "Test Campaign".to_string(), + campaign_description: "This is a test campaign".to_string(), + managers: vec![], + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(1000), + }, + }; + + execute( + deps.as_mut(), + mock_env(), + mock_info("creator", &[]), + create_campaign_msg.clone(), + )?; + + // Attempt to create another campaign with the same ID + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("creator", &[]), + create_campaign_msg, + ); + + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("Campaign already exists") => + { + Ok(()) + } + _ => Err(anyhow!( + "Expected 'Campaign already exists' error, found {:?}", + res + )), + } +} + +#[test] +fn execute_reward_users_valid() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Create a campaign + let campaign_id = "campaign1".to_string(); + execute( + deps.as_mut(), + env.clone(), + mock_info("creator", &[coin(10000, "token")]), + ExecuteMsg::CreateCampaign { + campaign_id: campaign_id.clone(), + campaign_name: "Campaign One".to_string(), + campaign_description: "The first campaign".to_string(), + managers: vec!["manager1".to_string()], + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(10000), + }, + }, + )?; + + // Reward users + let reward_users_msg = ExecuteMsg::RewardUsers { + campaign_id: campaign_id.clone(), + requests: vec![ + RewardUserRequest { + user_address: "user1".to_string(), + amount: Uint128::new(500), + }, + RewardUserRequest { + user_address: "user2".to_string(), + amount: Uint128::new(1500), + }, + ], + }; + execute( + deps.as_mut(), + env, + mock_info("creator", &[]), + reward_users_msg, + )?; + + // Verify user rewards and campaign state + let user1_rewards = + USER_REWARDS.load(deps.as_ref().storage, "user1".to_string())?; + assert_eq!( + user1_rewards, + Uint128::new(500), + "User1 rewards do not match." + ); + + let user2_rewards = + USER_REWARDS.load(deps.as_ref().storage, "user2".to_string())?; + assert_eq!( + user2_rewards, + Uint128::new(1500), + "User2 rewards do not match." + ); + + let updated_campaign = CAMPAIGN.load(deps.as_ref().storage, campaign_id)?; + assert_eq!( + updated_campaign.unallocated_amount, + Uint128::new(8000), + "Campaign unallocated amount does not match expected." + ); + + Ok(()) +} + +#[test] +fn execute_reward_users_insufficient_funds() -> TestResult { + let (mut deps, _env) = setup_with_block_time(0)?; + + // Create a campaign with limited funds + let campaign_id = "limited_fund_campaign"; + execute( + deps.as_mut(), + mock_env(), + mock_info("creator", &[coin(500, "token")]), + ExecuteMsg::CreateCampaign { + campaign_id: campaign_id.to_string(), + campaign_name: "Limited Fund Campaign".to_string(), + campaign_description: "This campaign has limited funds".to_string(), + managers: vec![], + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(500), + }, + }, + )?; + + // Attempt to reward users more than available funds + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("creator", &[]), + ExecuteMsg::RewardUsers { + campaign_id: campaign_id.to_string(), + requests: vec![RewardUserRequest { + user_address: "user1".to_string(), + amount: Uint128::new(600), // More than available + }], + }, + ); + + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("Not enough funds in the campaign") => + { + Ok(()) + } + _ => Err(anyhow!( + "Expected 'Not enough funds in the campaign' error, found {:?}", + res + )), + } +} + +#[test] +fn execute_claim_valid() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Register a vesting account first + let register_msg = ExecuteMsg::RegisterVestingAccount { + master_address: Some("owner".to_string()), + address: "user1".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(env.block.time.seconds() - 10), // Vesting started in the past + end_time: Uint64::new(env.block.time.seconds() + 10), // Ends in the future + vesting_amount: Uint128::new(1000), + }, + }; + let info = mock_info("owner", &[coin(1000, "token")]); + execute(deps.as_mut(), env.clone(), info, register_msg)?; + + // Attempt to claim tokens + let claim_msg = ExecuteMsg::Claim { + denoms: vec![Denom::Native("token".to_string())], + recipient: Some("recipient".to_string()), + }; + let info = mock_info("user1", &[]); + let response = execute(deps.as_mut(), env.clone(), info, claim_msg)?; + + // Verify that tokens are transferred + assert_eq!(response.messages.len(), 1, "Expected one message"); + print!("{:?}", response.messages); + + // Verify vesting and user states are updated + let account = VESTING_ACCOUNTS + .load(&deps.storage, ("user1", "token")) + .unwrap(); + assert_eq!(account.claimed_amount, Uint128::new(500)); + + Ok(()) +} + +#[test] +fn execute_claim_no_vesting_account() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Attempt to claim tokens without registering a vesting account + let claim_msg = ExecuteMsg::Claim { + denoms: vec![Denom::Native("token".to_string())], + recipient: Some("recipient".to_string()), + }; + let info = mock_info("user1", &[]); + let res = execute(deps.as_mut(), env.clone(), info, claim_msg); + + // Verify that it results in an error + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) => { + assert!( + msg.contains("vesting entry is not found for denom"), + "Unexpected error message: {}", + msg + ); + } + _ => return Err(anyhow!("Expected error, got {:?}", res)), + } + + Ok(()) +} + +#[test] +fn execute_withdraw_valid() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Create a campaign first + let create_campaign_msg = ExecuteMsg::CreateCampaign { + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(env.block.time.seconds()), + end_time: Uint64::new(env.block.time.seconds() + 100), + vesting_amount: Uint128::new(1000), + }, + campaign_id: "campaign1".to_string(), + campaign_name: "Test Campaign".to_string(), + campaign_description: "A campaign for testing".to_string(), + managers: vec!["manager1".to_string()], + }; + let info = mock_info("owner", &[coin(1000, "token")]); + execute(deps.as_mut(), env.clone(), info, create_campaign_msg)?; + + // Attempt to withdraw unallocated funds + let withdraw_msg = ExecuteMsg::Withdraw { + amount: Uint128::new(500), + campaign_id: "campaign1".to_string(), + }; + let info = mock_info("owner", &[]); + execute(deps.as_mut(), env.clone(), info, withdraw_msg)?; + + // Verify campaign unallocated amount is updated + let campaign = CAMPAIGN + .load(&deps.storage, "campaign1".to_string()) + .unwrap(); + assert_eq!( + campaign.unallocated_amount, + Uint128::new(500), + "Campaign unallocated amount not updated correctly" + ); + + Ok(()) +} + +#[test] +fn execute_withdraw_unauthorized() -> TestResult { + let (mut deps, env) = setup_with_block_time(100)?; + + // Create a campaign with some funds + create_test_campaign(&mut deps, &env, "campaign1", "owner"); + + // Attempt to withdraw funds from the contract by an unauthorized user + let msg = ExecuteMsg::Withdraw { + amount: Uint128::new(500), + campaign_id: "campaign1".to_string(), + }; + let info = mock_info("unauthorized_user", &[]); + let res = execute(deps.as_mut(), env.clone(), info, msg); + + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("Only contract owner can withdraw") => + { + Ok(()) + } + _ => Err(anyhow!("Expected unauthorized withdraw attempt to fail")), + } +} + +#[test] +fn execute_deactivate_campaign_authorized() -> TestResult { + let (mut deps, env) = setup_with_block_time(200)?; + + // Create a campaign and mark it active + create_test_campaign(&mut deps, &env, "campaign2", "owner"); + + // Deactivate the campaign by the owner + let msg = ExecuteMsg::DeactivateCampaign { + campaign_id: "campaign2".to_string(), + }; + let info = mock_info("owner", &[]); + execute(deps.as_mut(), env.clone(), info, msg)?; + + // Check if the campaign is deactivated + let campaign = + CAMPAIGN.load(deps.as_ref().storage, "campaign2".to_string())?; + assert_eq!(campaign.is_active, false, "Campaign should be deactivated"); + + Ok(()) +} + +#[test] +fn execute_deactivate_campaign_unauthorized() -> TestResult { + let (mut deps, env) = setup_with_block_time(300)?; + + // Create a campaign and mark it active + create_test_campaign(&mut deps, &env, "campaign3", "owner"); + + // Attempt to deactivate the campaign by an unauthorized user + let msg = ExecuteMsg::DeactivateCampaign { + campaign_id: "campaign3".to_string(), + }; + let info = mock_info("unauthorized_user", &[]); + let res = execute(deps.as_mut(), env, info, msg); + + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("Unauthorized") => + { + Ok(()) + } + _ => Err(anyhow!( + "Expected unauthorized deactivation attempt to fail" + )), + } +} + +// Helper function to create a test campaign +fn create_test_campaign( + deps: &mut OwnedDeps, + env: &Env, + campaign_id: &str, + owner: &str, +) { + let msg = ExecuteMsg::CreateCampaign { + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(env.block.time.seconds() + 100), + end_time: Uint64::new(env.block.time.seconds() + 200), + vesting_amount: Uint128::new(1000), + }, + campaign_id: campaign_id.to_string(), + campaign_name: "Test Campaign".to_string(), + campaign_description: "A campaign for testing".to_string(), + managers: vec![owner.to_string()], + }; + let info = mock_info(owner, &[coin(1000, "token")]); + let _ = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); +} diff --git a/contracts/core-token-vesting/tests/tests/test_manager.rs b/contracts/core-token-vesting/tests/tests/test_manager.rs new file mode 100644 index 0000000..e2e113e --- /dev/null +++ b/contracts/core-token-vesting/tests/tests/test_manager.rs @@ -0,0 +1,149 @@ +use anyhow::anyhow; +use cosmwasm_std::{ + coin, + testing::{self, MockApi, MockQuerier, MockStorage}, + Empty, Env, OwnedDeps, StdError, Timestamp, Uint128, Uint64, +}; +use cw20::Denom; +use token_vesting::{ + contract::{execute, instantiate}, + errors::ContractError, + msg::{ExecuteMsg, InstantiateMsg, VestingSchedule}, +}; + +use super::helpers::TestResult; + +pub fn mock_env_with_time(block_time: u64) -> Env { + let mut env = testing::mock_env(); + env.block.time = Timestamp::from_seconds(block_time); + env +} + +/// Convenience function for instantiating the contract at and setting up +/// the env to have the given block time. +pub fn setup_with_block_time( + block_time: u64, +) -> anyhow::Result<(OwnedDeps, Env)> { + let mut deps = testing::mock_dependencies(); + let env = mock_env_with_time(block_time); + instantiate( + deps.as_mut(), + env.clone(), + testing::mock_info("admin-sender", &[]), + InstantiateMsg {}, + )?; + Ok((deps, env)) +} + +#[test] +fn deregister_err_nonexistent_vesting_account() -> TestResult { + let (mut deps, _env) = setup_with_block_time(0)?; + + let msg = ExecuteMsg::DeregisterVestingAccount { + address: "nonexistent".to_string(), + denom: Denom::Native("token".to_string()), + vested_token_recipient: None, + left_vesting_token_recipient: None, + }; + + let res = execute( + deps.as_mut(), + testing::mock_env(), + testing::mock_info("admin-sender", &[]), + msg, + ); + + match res { + Ok(_) => Err(anyhow!("Unexpected result: {:#?}", res)), + Err(ContractError::Std(StdError::GenericErr { msg, .. })) => { + assert!(msg.contains("vesting entry is not found for denom")); + Ok(()) + } + Err(err) => Err(anyhow!("Unexpected error: {:#?}", err)), + } +} + +#[test] +fn deregister_err_unauthorized_vesting_account() -> TestResult { + // Set up the environment with a block time before the vesting start time + let (mut deps, env) = setup_with_block_time(50)?; + + let register_msg = ExecuteMsg::RegisterVestingAccount { + master_address: Some("addr0002".to_string()), + address: "addr0001".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(110), + vesting_amount: Uint128::new(1000000u128), + }, + }; + + execute( + deps.as_mut(), + env.clone(), // Use the custom environment with the adjusted block time + testing::mock_info("admin-sender", &[coin(1000000, "token")]), + register_msg, + )?; + + // Try to deregister with unauthorized sender + let msg = ExecuteMsg::DeregisterVestingAccount { + address: "addr0001".to_string(), + denom: Denom::Native("token".to_string()), + vested_token_recipient: None, + left_vesting_token_recipient: None, + }; + + let res = execute( + deps.as_mut(), + env, // Use the custom environment with the adjusted block time + testing::mock_info("addr0003", &[]), + msg, + ); + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg == "unauthorized" => {} + _ => return Err(anyhow!("Unexpected result: {:?}", res)), + } + + Ok(()) +} + +#[test] +fn deregister_successful() -> TestResult { + // Set up the environment with a block time before the vesting start time + let (mut deps, env) = setup_with_block_time(50)?; + + let register_msg = ExecuteMsg::RegisterVestingAccount { + master_address: Some("addr0002".to_string()), + address: "addr0001".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(110), + vesting_amount: Uint128::new(1000000u128), + }, + }; + + execute( + deps.as_mut(), + env.clone(), // Use the custom environment with the adjusted block time + testing::mock_info("admin-sender", &[coin(1000000, "token")]), + register_msg, + )?; + + // Deregister with the master address + let msg = ExecuteMsg::DeregisterVestingAccount { + address: "addr0001".to_string(), + denom: Denom::Native("token".to_string()), + vested_token_recipient: None, + left_vesting_token_recipient: None, + }; + + let _res = execute( + deps.as_mut(), + env, // Use the custom environment with the adjusted block time + testing::mock_info("addr0002", &[]), + msg, + )?; + + Ok(()) +} diff --git a/contracts/core-token-vesting/src/testing.rs b/contracts/core-token-vesting/tests/tests/test_vesting.rs similarity index 94% rename from contracts/core-token-vesting/src/testing.rs rename to contracts/core-token-vesting/tests/tests/test_vesting.rs index 02986a4..378b378 100644 --- a/contracts/core-token-vesting/src/testing.rs +++ b/contracts/core-token-vesting/tests/tests/test_vesting.rs @@ -1,20 +1,20 @@ -use crate::contract::tests::TestResult; -use crate::contract::{execute, instantiate, query}; -use crate::errors::{CliffError, ContractError, VestingError}; -use crate::msg::{ +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, Denom}; +use token_vesting::contract::{execute, instantiate, query}; +use token_vesting::errors::{CliffError, ContractError, VestingError}; +use token_vesting::msg::{ Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, VestingAccountResponse, VestingData, VestingSchedule, }; +use super::helpers::TestResult; use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage}; use cosmwasm_std::MessageInfo; use cosmwasm_std::{ from_json, testing::{mock_dependencies, mock_env, mock_info}, - to_json_binary, Addr, Attribute, BankMsg, Coin, Env, OwnedDeps, Response, - StdError, SubMsg, Timestamp, Uint128, Uint64, WasmMsg, + to_json_binary, Addr, Attribute, BankMsg, Coin, Response, SubMsg, WasmMsg, }; -use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, Denom}; +use cosmwasm_std::{Env, OwnedDeps, StdError, Timestamp, Uint128, Uint64}; #[test] fn proper_initialization() -> TestResult { @@ -873,3 +873,38 @@ fn query_vesting_account() -> TestResult { ); Ok(()) } + +#[test] +fn linear_vesting_vested_amount() -> TestResult { + let schedule = VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(110), + vesting_amount: Uint128::new(1000000u128), + }; + + assert_eq!(schedule.vested_amount(100)?, Uint128::zero()); + assert_eq!(schedule.vested_amount(105)?, Uint128::new(500000u128)); + assert_eq!(schedule.vested_amount(110)?, Uint128::new(1000000u128)); + assert_eq!(schedule.vested_amount(115)?, Uint128::new(1000000u128)); + + Ok(()) +} + +#[test] +fn linear_vesting_with_cliff_vested_amount() -> TestResult { + let schedule = VestingSchedule::LinearVestingWithCliff { + start_time: Uint64::new(100), + end_time: Uint64::new(110), + vesting_amount: Uint128::new(1_000_000_u128), + cliff_amount: Uint128::new(100_000_u128), + cliff_time: Uint64::new(105), + }; + + assert_eq!(schedule.vested_amount(100)?, Uint128::zero()); + assert_eq!(schedule.vested_amount(105)?, Uint128::new(100000u128)); // cliff time then the cliff amount + assert_eq!(schedule.vested_amount(120)?, Uint128::new(1000000u128)); // complete vesting + assert_eq!(schedule.vested_amount(104)?, Uint128::zero()); // before cliff time + assert_eq!(schedule.vested_amount(109)?, Uint128::new(820_000)); // after cliff time but before end time + + Ok(()) +} From 04189732d49d94278366c88993dfb37ac396c4ac Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Tue, 6 Feb 2024 23:11:30 +0100 Subject: [PATCH 05/17] fix: fix tests --- contracts/core-token-vesting/src/contract.rs | 41 +-- contracts/core-token-vesting/src/state.rs | 1 + .../tests/tests/test_airdrop.rs | 262 +----------------- .../tests/tests/test_vesting.rs | 2 +- 4 files changed, 22 insertions(+), 284 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 8003076..f928ddf 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -144,10 +144,10 @@ fn deactivate_campaign( campaign.is_active = false; CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; - let bond_denom = deps.querier.query_bonded_denom()?; + let denom = to_string(&campaign.denom).unwrap(); let own_balance: Uint128 = deps .querier - .query_balance(&env.contract.address, bond_denom.clone()) + .query_balance(&env.contract.address, denom) .map_err(|_| StdError::generic_err("Failed to query contract balance"))? .amount; @@ -263,6 +263,7 @@ fn create_campaign( owner: info.sender.into_string(), managers: managers, unallocated_amount: coin.amount, + denom: Denom::Native(coin.denom.clone()), is_active: true, }; CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; @@ -594,7 +595,7 @@ fn vesting_account( /// tokens to the contract owner's account. pub fn withdraw( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, amount: Uint128, campaign_id: &str, @@ -607,34 +608,6 @@ pub fn withdraw( ); } - let bond_denom = deps.querier.query_bonded_denom()?; - - let own_balance: Uint128 = deps - .querier - .query_balance(env.contract.address, bond_denom.clone()) - .map_err(|_| { - ContractError::Std(StdError::generic_err( - "Failed to query contract balance", - )) - })? - .amount; - - if amount > own_balance { - return Err( - StdError::generic_err("Not enough funds in the contract").into() - ); - } - - let res = Response::new() - .add_attribute("method", "withdraw") - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin { - denom: bond_denom.clone(), - amount, - }], - })); - // Update campaign unallocated amount if amount > campaign.unallocated_amount { let update_result = CAMPAIGN.update( @@ -672,5 +645,9 @@ pub fn withdraw( } } - return Ok(res); + Ok(Response::new().add_messages(vec![build_send_msg( + campaign.denom, + amount, + info.sender.to_string(), + )?])) } diff --git a/contracts/core-token-vesting/src/state.rs b/contracts/core-token-vesting/src/state.rs index 9eddd58..c6065d3 100644 --- a/contracts/core-token-vesting/src/state.rs +++ b/contracts/core-token-vesting/src/state.rs @@ -27,6 +27,7 @@ pub struct Campaign { pub campaign_description: String, pub unallocated_amount: Uint128, + pub denom: Denom, pub owner: String, pub managers: Vec, diff --git a/contracts/core-token-vesting/tests/tests/test_airdrop.rs b/contracts/core-token-vesting/tests/tests/test_airdrop.rs index c2d4395..c764714 100644 --- a/contracts/core-token-vesting/tests/tests/test_airdrop.rs +++ b/contracts/core-token-vesting/tests/tests/test_airdrop.rs @@ -2,226 +2,18 @@ use anyhow::anyhow; use cosmwasm_std::{ coin, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - Empty, Env, OwnedDeps, StdError, Uint128, Uint64, + Addr, Empty, Env, OwnedDeps, StdError, Uint128, Uint64, }; use cw20::Denom; use token_vesting::{ contract::execute, errors::ContractError, msg::{ExecuteMsg, RewardUserRequest, VestingSchedule}, - state::{denom_to_key, CAMPAIGN, USER_REWARDS, VESTING_ACCOUNTS}, + state::{CAMPAIGN, USER_REWARDS}, }; use super::{helpers::TestResult, test_manager::setup_with_block_time}; -#[test] -fn execute_register_vesting_account_valid() -> TestResult { - let (mut deps, env) = setup_with_block_time(100)?; - - let register_msg = ExecuteMsg::RegisterVestingAccount { - master_address: Some("master".to_string()), - address: "addr_valid".to_string(), - vesting_schedule: VestingSchedule::LinearVesting { - start_time: Uint64::new(100), - end_time: Uint64::new(200), - vesting_amount: Uint128::new(1000), - }, - }; - - let res = execute( - deps.as_mut(), - env, - mock_info("sender", &[coin(1000, "token")]), - register_msg, - ); - - assert!( - res.is_ok(), - "Expected successful registration, got: {:?}", - res.err() - ); - - // Verify the vesting account is correctly registered in the contract's state - let vesting_account = VESTING_ACCOUNTS.load( - deps.as_ref().storage, - ( - "addr_valid", - &denom_to_key(Denom::Native("token".to_string())), - ), - )?; - assert_eq!( - vesting_account.vesting_amount, - Uint128::new(1000), - "Vesting amount mismatch" - ); - - Ok(()) -} - -#[test] -fn execute_register_vesting_account_duplicate() -> TestResult { - let (mut deps, env) = setup_with_block_time(100)?; - - let register_msg_first = ExecuteMsg::RegisterVestingAccount { - master_address: Some("master".to_string()), - address: "addr_duplicate".to_string(), - vesting_schedule: VestingSchedule::LinearVesting { - start_time: Uint64::new(100), - end_time: Uint64::new(200), - vesting_amount: Uint128::new(1000), - }, - }; - - // First registration should succeed - let _ = execute( - deps.as_mut(), - env.clone(), - mock_info("sender", &[coin(1000, "token")]), - register_msg_first.clone(), - )?; - - // Attempt to register again with the same address and token denomination - let register_msg_second = register_msg_first.clone(); - let res = execute( - deps.as_mut(), - env, - mock_info("sender", &[coin(1000, "token")]), - register_msg_second, - ); - - assert!( - matches!(res, Err(ContractError::Std(StdError::GenericErr { .. }))), - "Expected duplicate registration error, got: {:?}", - res - ); - - Ok(()) -} - -#[test] -fn execute_register_vesting_account_invalid_deposit() -> TestResult { - let (mut deps, env) = setup_with_block_time(100)?; - - // Attempt registration with invalid deposit details - let res = execute( - deps.as_mut(), - env, - mock_info("addr_sender", &[coin(500, "invalid_token")]), // Incorrect token denomination - ExecuteMsg::RegisterVestingAccount { - master_address: Some("addr_master".to_string()), - address: "addr_vesting".to_string(), - vesting_schedule: VestingSchedule::LinearVesting { - start_time: Uint64::new(100), - end_time: Uint64::new(200), - vesting_amount: Uint128::new(1000u128), - }, - }, - ); - - assert!( - matches!(res, Err(ContractError::Std(StdError::GenericErr { msg, .. })) if msg.contains("Only native tokens are allowed")) - ); - - Ok(()) -} - -#[test] -fn execute_deregister_vesting_account_authorized() -> TestResult { - let (mut deps, env) = setup_with_block_time(100)?; - - // Simulate registering a vesting account first - let vesting_account_address = "vesting_account"; - let master_address = "master_address"; - let register_msg = ExecuteMsg::RegisterVestingAccount { - master_address: Some(master_address.to_string()), - address: vesting_account_address.to_string(), - vesting_schedule: VestingSchedule::LinearVesting { - start_time: Uint64::new(50), - end_time: Uint64::new(150), - vesting_amount: Uint128::new(1000), - }, - }; - execute( - deps.as_mut(), - env.clone(), - mock_info("creator", &[coin(1000, "token")]), - register_msg, - )?; - - // Attempt to deregister the vesting account by the master address - let deregister_msg = ExecuteMsg::DeregisterVestingAccount { - address: vesting_account_address.to_string(), - denom: Denom::Native("token".to_string()), - vested_token_recipient: Some("recipient1".to_string()), - left_vesting_token_recipient: Some("recipient2".to_string()), - }; - let res = execute( - deps.as_mut(), - env, - mock_info(master_address, &[]), - deregister_msg, - )?; - - // Assertions to verify the expected state changes and messages - assert_eq!( - res.messages.len(), - 2, - "Expected two bank send messages for token transfer" - ); - assert!( - res.attributes.iter().any(|attr| attr.key == "action" - && attr.value == "deregister_vesting_account"), - "Expected 'deregister_vesting_account' action in response attributes" - ); - - Ok(()) -} - -#[test] -fn execute_deregister_vesting_account_unauthorized() -> TestResult { - let (mut deps, env) = setup_with_block_time(100)?; - - // Simulate registering a vesting account first - let vesting_account_address = "vesting_account"; - let master_address = "master_address"; - let register_msg = ExecuteMsg::RegisterVestingAccount { - master_address: Some(master_address.to_string()), - address: vesting_account_address.to_string(), - vesting_schedule: VestingSchedule::LinearVesting { - start_time: Uint64::new(50), - end_time: Uint64::new(150), - vesting_amount: Uint128::new(1000), - }, - }; - execute( - deps.as_mut(), - env.clone(), - mock_info("creator", &[coin(1000, "token")]), - register_msg, - )?; - - // Attempt to deregister the vesting account by an unauthorized user - let deregister_msg = ExecuteMsg::DeregisterVestingAccount { - address: vesting_account_address.to_string(), - denom: Denom::Native("token".to_string()), - vested_token_recipient: None, - left_vesting_token_recipient: None, - }; - let res = execute( - deps.as_mut(), - env, - mock_info("unauthorized_user", &[]), - deregister_msg, - ); - - assert!( - matches!(res, Err(ContractError::Std(StdError::GenericErr { msg, .. })) if msg.contains("unauthorized")), - "Expected an unauthorized error" - ); - - Ok(()) -} - #[test] fn execute_create_campaign_valid() -> TestResult { let (mut deps, env) = setup_with_block_time(0)?; @@ -281,7 +73,7 @@ fn execute_create_campaign_duplicate_id() -> TestResult { execute( deps.as_mut(), mock_env(), - mock_info("creator", &[]), + mock_info("creator", &[coin(5000, "token")]), create_campaign_msg.clone(), )?; @@ -289,7 +81,7 @@ fn execute_create_campaign_duplicate_id() -> TestResult { let res = execute( deps.as_mut(), mock_env(), - mock_info("creator", &[]), + mock_info("creator", &[coin(5000, "token")]), create_campaign_msg, ); @@ -427,44 +219,6 @@ fn execute_reward_users_insufficient_funds() -> TestResult { } } -#[test] -fn execute_claim_valid() -> TestResult { - let (mut deps, env) = setup_with_block_time(0)?; - - // Register a vesting account first - let register_msg = ExecuteMsg::RegisterVestingAccount { - master_address: Some("owner".to_string()), - address: "user1".to_string(), - vesting_schedule: VestingSchedule::LinearVesting { - start_time: Uint64::new(env.block.time.seconds() - 10), // Vesting started in the past - end_time: Uint64::new(env.block.time.seconds() + 10), // Ends in the future - vesting_amount: Uint128::new(1000), - }, - }; - let info = mock_info("owner", &[coin(1000, "token")]); - execute(deps.as_mut(), env.clone(), info, register_msg)?; - - // Attempt to claim tokens - let claim_msg = ExecuteMsg::Claim { - denoms: vec![Denom::Native("token".to_string())], - recipient: Some("recipient".to_string()), - }; - let info = mock_info("user1", &[]); - let response = execute(deps.as_mut(), env.clone(), info, claim_msg)?; - - // Verify that tokens are transferred - assert_eq!(response.messages.len(), 1, "Expected one message"); - print!("{:?}", response.messages); - - // Verify vesting and user states are updated - let account = VESTING_ACCOUNTS - .load(&deps.storage, ("user1", "token")) - .unwrap(); - assert_eq!(account.claimed_amount, Uint128::new(500)); - - Ok(()) -} - #[test] fn execute_claim_no_vesting_account() -> TestResult { let (mut deps, env) = setup_with_block_time(0)?; @@ -508,9 +262,15 @@ fn execute_withdraw_valid() -> TestResult { campaign_description: "A campaign for testing".to_string(), managers: vec!["manager1".to_string()], }; - let info = mock_info("owner", &[coin(1000, "token")]); + let info = mock_info("owner", &[coin(1000, "denom")]); execute(deps.as_mut(), env.clone(), info, create_campaign_msg)?; + // fund the contract manually + deps.querier.update_balance( + Addr::unchecked(&env.contract.address), + vec![coin(1000, "denom")], + ); + // Attempt to withdraw unallocated funds let withdraw_msg = ExecuteMsg::Withdraw { amount: Uint128::new(500), diff --git a/contracts/core-token-vesting/tests/tests/test_vesting.rs b/contracts/core-token-vesting/tests/tests/test_vesting.rs index 378b378..d4cc0e9 100644 --- a/contracts/core-token-vesting/tests/tests/test_vesting.rs +++ b/contracts/core-token-vesting/tests/tests/test_vesting.rs @@ -142,7 +142,7 @@ fn register_cliff_vesting_account_with_native_token() -> TestResult { Ok(()) } -fn require_error( +pub fn require_error( deps: &mut OwnedDeps, env: &Env, info: MessageInfo, From 5543049b1650511e5200d08296c191da8e6965d0 Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Wed, 7 Feb 2024 14:58:03 +0100 Subject: [PATCH 06/17] fix: fix comments --- contracts/core-token-vesting/src/contract.rs | 40 ++-- .../tests/tests/test_airdrop.rs | 223 +++++++++++++++++- 2 files changed, 238 insertions(+), 25 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index f928ddf..0c98b51 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -186,19 +186,13 @@ fn reward_users( return Err(StdError::generic_err("Campaign is not active").into()); } - let mut unallocated_amount = campaign.unallocated_amount; - + let total_requested: Uint128 = requests.iter().map(|req| req.amount).sum(); + if total_requested > campaign.unallocated_amount { + return Err( + StdError::generic_err("Insufficient funds for all rewards").into() + ); + } for req in requests { - if unallocated_amount < req.amount { - // We fail on the first request that cannot be fulfilled - // This is to ensure that we do not partially fulfill requests - // and leave the campaign in an inconsistent state. - return Err(StdError::generic_err( - "Not enough funds in the campaign", - ) - .into()); - } - match USER_REWARDS.may_load(deps.storage, req.user_address.clone())? { Some(mut user_reward) => { user_reward += req.amount; @@ -216,8 +210,6 @@ fn reward_users( )?; } }; - unallocated_amount -= req.amount; - res.push(RewardUserResponse { user_address: req.user_address.clone(), success: true, @@ -225,7 +217,7 @@ fn reward_users( }); } - campaign.unallocated_amount = unallocated_amount; + campaign.unallocated_amount = campaign.unallocated_amount - total_requested; CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; Ok(Response::new() @@ -251,7 +243,7 @@ fn create_campaign( } if info.funds.len() != 1 { - return Err(StdError::generic_err("Only one coin is allowed").into()); + return Err(StdError::generic_err("one denom sent required").into()); } let coin = info.funds.get(0).unwrap(); @@ -645,9 +637,15 @@ pub fn withdraw( } } - Ok(Response::new().add_messages(vec![build_send_msg( - campaign.denom, - amount, - info.sender.to_string(), - )?])) + Ok(Response::new() + .add_messages(vec![build_send_msg( + campaign.denom, + amount, + info.sender.to_string(), + )?]) + .add_attribute("withdraw", &amount.to_string()) + .add_attribute( + "campaign_unallocated_amount", + &campaign.unallocated_amount.to_string(), + )) } diff --git a/contracts/core-token-vesting/tests/tests/test_airdrop.rs b/contracts/core-token-vesting/tests/tests/test_airdrop.rs index c764714..83cd88f 100644 --- a/contracts/core-token-vesting/tests/tests/test_airdrop.rs +++ b/contracts/core-token-vesting/tests/tests/test_airdrop.rs @@ -99,7 +99,79 @@ fn execute_create_campaign_duplicate_id() -> TestResult { } #[test] -fn execute_reward_users_valid() -> TestResult { +fn execute_create_campaign_invalid_coin_count() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Create a campaign with invalid coin count + let create_campaign_msg = ExecuteMsg::CreateCampaign { + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(5000), + }, + campaign_id: "campaign1".to_string(), + campaign_name: "Test Campaign".to_string(), + campaign_description: "A test campaign".to_string(), + managers: vec!["manager1".to_string(), "manager2".to_string()], + }; + let res = execute( + deps.as_mut(), + env, + mock_info("creator", &[]), + create_campaign_msg, + ); + + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("one denom sent required") => + { + Ok(()) + } + _ => Err(anyhow!( + "Expected 'one denom sent required' error, found {:?}", + res + )), + } +} + +#[test] +fn execute_create_campaign_2_coins() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Create a campaign with 2 coins + let create_campaign_msg = ExecuteMsg::CreateCampaign { + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(5000), + }, + campaign_id: "campaign2".to_string(), + campaign_name: "Test Campaign".to_string(), + campaign_description: "A test campaign".to_string(), + managers: vec!["manager1".to_string(), "manager2".to_string()], + }; + let res = execute( + deps.as_mut(), + env, + mock_info("creator", &[coin(5000, "token"), coin(5000, "token")]), + create_campaign_msg, + ); + + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("one denom sent required") => + { + Ok(()) + } + _ => Err(anyhow!( + "Expected 'one denom sent required' error, found {:?}", + res + )), + } +} + +#[test] +fn execute_reward_users_unactive_campaign() -> TestResult { let (mut deps, env) = setup_with_block_time(0)?; // Create a campaign @@ -121,6 +193,13 @@ fn execute_reward_users_valid() -> TestResult { }, )?; + // Deactivate the campaign + let msg = ExecuteMsg::DeactivateCampaign { + campaign_id: campaign_id.clone(), + }; + let info = mock_info("creator", &[]); + execute(deps.as_mut(), env.clone(), info, msg)?; + // Reward users let reward_users_msg = ExecuteMsg::RewardUsers { campaign_id: campaign_id.clone(), @@ -135,9 +214,120 @@ fn execute_reward_users_valid() -> TestResult { }, ], }; + let res = execute( + deps.as_mut(), + env, + mock_info("creator", &[]), + reward_users_msg, + ); + + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("Campaign is not active") => + { + Ok(()) + } + _ => Err(anyhow!( + "Expected 'Campaign is not active' error, found {:?}", + res + )), + } +} + +#[test] +fn execute_reward_users_unauthorized() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Create a campaign + let campaign_id = "campaign1".to_string(); execute( + deps.as_mut(), + env.clone(), + mock_info("creator", &[coin(10000, "token")]), + ExecuteMsg::CreateCampaign { + campaign_id: campaign_id.clone(), + campaign_name: "Campaign One".to_string(), + campaign_description: "The first campaign".to_string(), + managers: vec!["manager1".to_string()], + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(10000), + }, + }, + )?; + + // Reward users + let reward_users_msg = ExecuteMsg::RewardUsers { + campaign_id: campaign_id.clone(), + requests: vec![ + RewardUserRequest { + user_address: "user1".to_string(), + amount: Uint128::new(500), + }, + RewardUserRequest { + user_address: "user2".to_string(), + amount: Uint128::new(1500), + }, + ], + }; + let res = execute( deps.as_mut(), env, + mock_info("unauthorized_user", &[]), + reward_users_msg, + ); + + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("Unauthorized") => + { + Ok(()) + } + _ => Err(anyhow!("Expected 'Unauthorized' error, found {:?}", res)), + } +} + +#[test] +fn execute_reward_users_valid() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Create a campaign + let campaign_id = "campaign1".to_string(); + execute( + deps.as_mut(), + env.clone(), + mock_info("creator", &[coin(10000, "token")]), + ExecuteMsg::CreateCampaign { + campaign_id: campaign_id.clone(), + campaign_name: "Campaign One".to_string(), + campaign_description: "The first campaign".to_string(), + managers: vec!["manager1".to_string()], + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(10000), + }, + }, + )?; + + // Reward users + let reward_users_msg = ExecuteMsg::RewardUsers { + campaign_id: campaign_id.clone(), + requests: vec![ + RewardUserRequest { + user_address: "user1".to_string(), + amount: Uint128::new(500), + }, + RewardUserRequest { + user_address: "user2".to_string(), + amount: Uint128::new(1500), + }, + ], + }; + execute( + deps.as_mut(), + env.clone(), mock_info("creator", &[]), reward_users_msg, )?; @@ -159,13 +349,38 @@ fn execute_reward_users_valid() -> TestResult { "User2 rewards do not match." ); - let updated_campaign = CAMPAIGN.load(deps.as_ref().storage, campaign_id)?; + let updated_campaign = + CAMPAIGN.load(deps.as_ref().storage, campaign_id.clone())?; assert_eq!( updated_campaign.unallocated_amount, Uint128::new(8000), "Campaign unallocated amount does not match expected." ); + // Additional reward + let reward_users_msg = ExecuteMsg::RewardUsers { + campaign_id: campaign_id.clone(), + requests: vec![RewardUserRequest { + user_address: "user1".to_string(), + amount: Uint128::new(1000), + }], + }; + execute( + deps.as_mut(), + env, + mock_info("creator", &[]), + reward_users_msg, + )?; + + // Verify user rewards and campaign state + let user1_rewards = + USER_REWARDS.load(deps.as_ref().storage, "user1".to_string())?; + assert_eq!( + user1_rewards, + Uint128::new(1500), + "User1 rewards do not match." + ); + Ok(()) } @@ -208,12 +423,12 @@ fn execute_reward_users_insufficient_funds() -> TestResult { match res { Err(ContractError::Std(StdError::GenericErr { msg, .. })) - if msg.contains("Not enough funds in the campaign") => + if msg.contains("Insufficient funds for all rewards") => { Ok(()) } _ => Err(anyhow!( - "Expected 'Not enough funds in the campaign' error, found {:?}", + "Expected 'Insufficient funds for all rewards' error, found {:?}", res )), } From 51eafe44a2f243dbe507be9f93b3daa7865ee43e Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Wed, 7 Feb 2024 15:50:55 +0100 Subject: [PATCH 07/17] fix: add more code cov --- contracts/core-token-vesting/src/contract.rs | 24 +++++---- .../tests/tests/test_manager.rs | 40 ++++++++++++++ .../tests/tests/test_vesting.rs | 54 +++++++++++++++++++ 3 files changed, 109 insertions(+), 9 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 0c98b51..1835bf7 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -285,18 +285,19 @@ fn register_vesting_account( // validate vesting schedule vesting_schedule.validate(block_time, deposit_amount)?; + let vesting_account = VestingAccount { + master_address: master_address.clone(), + address: address.to_string(), + vesting_denom: deposit_denom.clone(), + vesting_amount: deposit_amount, + vesting_schedule, + claimed_amount: Uint128::zero(), + }; VESTING_ACCOUNTS.save( storage, (address.as_str(), &denom_key), - &VestingAccount { - master_address: master_address.clone(), - address: address.to_string(), - vesting_denom: deposit_denom.clone(), - vesting_amount: deposit_amount, - vesting_schedule, - claimed_amount: Uint128::zero(), - }, + &vesting_account, )?; Ok(Response::new().add_attributes(vec![ @@ -352,6 +353,11 @@ fn deregister_vesting_account( // transfer already vested but not claimed amount to // a account address or the given `vested_token_recipient` address + print!( + "claimed_amount: {}", + vested_amount.checked_sub(claimed_amount)? + ); + let claimable_amount = vested_amount.checked_sub(claimed_amount)?; if !claimable_amount.is_zero() { let recipient = @@ -581,7 +587,7 @@ fn vesting_account( Ok(VestingAccountResponse { address, vestings }) } -/// Allow the contract owner to withdraw native tokens +/// Allow the contract owner to withdraw the funds of the campaign /// /// Ensures the requested amount is available in the contract balance. Transfers /// tokens to the contract owner's account. diff --git a/contracts/core-token-vesting/tests/tests/test_manager.rs b/contracts/core-token-vesting/tests/tests/test_manager.rs index e2e113e..c274533 100644 --- a/contracts/core-token-vesting/tests/tests/test_manager.rs +++ b/contracts/core-token-vesting/tests/tests/test_manager.rs @@ -147,3 +147,43 @@ fn deregister_successful() -> TestResult { Ok(()) } + +#[test] +fn deregister_successful_with_funds() -> TestResult { + // Set up the environment with a block time before the vesting start time + let (mut deps, env) = setup_with_block_time(50)?; + + let register_msg = ExecuteMsg::RegisterVestingAccount { + master_address: Some("addr0002".to_string()), + address: "addr0001".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(10), + end_time: Uint64::new(1100000), + vesting_amount: Uint128::new(1000000u128), + }, + }; + + execute( + deps.as_mut(), + env.clone(), // Use the custom environment with the adjusted block time + testing::mock_info("admin-sender", &[coin(1000000, "token")]), + register_msg, + )?; + + // Deregister with the master address + let msg = ExecuteMsg::DeregisterVestingAccount { + address: "addr0001".to_string(), + denom: Denom::Native("token".to_string()), + vested_token_recipient: None, + left_vesting_token_recipient: None, + }; + + let _res = execute( + deps.as_mut(), + env, // Use the custom environment with the adjusted block time + testing::mock_info("addr0002", &[]), + msg, + )?; + + Ok(()) +} diff --git a/contracts/core-token-vesting/tests/tests/test_vesting.rs b/contracts/core-token-vesting/tests/tests/test_vesting.rs index d4cc0e9..e8f986a 100644 --- a/contracts/core-token-vesting/tests/tests/test_vesting.rs +++ b/contracts/core-token-vesting/tests/tests/test_vesting.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, Denom}; use token_vesting::contract::{execute, instantiate, query}; use token_vesting::errors::{CliffError, ContractError, VestingError}; @@ -284,6 +285,59 @@ fn register_vesting_account_with_native_token() -> TestResult { Ok(()) } +#[test] +fn register_same_address_twice_error() -> TestResult { + let mut deps = mock_dependencies(); + let _res = instantiate( + deps.as_mut(), + mock_env(), + mock_info("addr0000", &[]), + InstantiateMsg {}, + )?; + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100); + + // valid amount + let msg = ExecuteMsg::RegisterVestingAccount { + master_address: None, + address: "addr0001".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(110), + vesting_amount: Uint128::new(1000000u128), + }, + }; + + let info = mock_info("addr0000", &[Coin::new(1000000u128, "uusd")]); + let _ = execute(deps.as_mut(), env.clone(), info, msg)?; + + // make time to half claimable + env.block.time = Timestamp::from_seconds(105); + + // valid amount + let msg = ExecuteMsg::RegisterVestingAccount { + master_address: None, + address: "addr0001".to_string(), + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(110), + vesting_amount: Uint128::new(1000000u128), + }, + }; + + let info = mock_info("addr0000", &[Coin::new(1000000u128, "uusd")]); + let res = execute(deps.as_mut(), env.clone(), info, msg); + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("already exists") => + { + Ok(()) + } + _ => Err(anyhow!("Expected 'already exits' error, found {:?}", res)), + } +} + #[test] fn register_vesting_account_with_cw20_token() -> TestResult { let mut deps = mock_dependencies(); From 797a260098a0037e4e18a660f484de9b768a0eac Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Wed, 7 Feb 2024 17:42:58 +0100 Subject: [PATCH 08/17] feat: add option to register vesting contract --- contracts/core-token-vesting/src/contract.rs | 47 ++++++++++++++------ contracts/core-token-vesting/src/state.rs | 3 ++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 1835bf7..25b7101 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -1,3 +1,5 @@ +use std::cmp::min; + #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -155,12 +157,29 @@ fn deactivate_campaign( } fn claim_campaign( - _deps: DepsMut, - _env: Env, - _info: MessageInfo, - _campaign_id: String, + deps: DepsMut, + env: Env, + info: MessageInfo, + campaign_id: String, ) -> Result { - todo!() + let user_reward = + USER_REWARDS + .load(deps.storage, info.sender.to_string()) + .map_err(|_| StdError::generic_err("Failed to load user rewards"))?; + + let campaign = CAMPAIGN + .load(deps.storage, campaign_id.clone()) + .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; + + register_vesting_account( + deps.storage, + env.block.time, + Some(campaign.owner), + info.sender.into_string(), + campaign.denom, + user_reward, + campaign.vesting_schedule, + ) } fn reward_users( @@ -229,7 +248,7 @@ fn create_campaign( deps: DepsMut, _env: Env, info: MessageInfo, - _vesting_schedule: VestingSchedule, + vesting_schedule: VestingSchedule, campaign_id: String, campaign_name: String, campaign_description: String, @@ -256,6 +275,7 @@ fn create_campaign( managers: managers, unallocated_amount: coin.amount, denom: Denom::Native(coin.denom.clone()), + vesting_schedule: vesting_schedule, is_active: true, }; CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; @@ -264,6 +284,7 @@ fn create_campaign( .add_attribute("method", "create_campaign") .add_attribute("campaign_id", campaign_id) .add_attribute("campaign_name", &campaign.campaign_name) + .add_attribute("campaign_description", &campaign.campaign_description) .add_attribute("initial_unallocated_amount", &coin.amount.to_string())) } @@ -353,11 +374,6 @@ fn deregister_vesting_account( // transfer already vested but not claimed amount to // a account address or the given `vested_token_recipient` address - print!( - "claimed_amount: {}", - vested_amount.checked_sub(claimed_amount)? - ); - let claimable_amount = vested_amount.checked_sub(claimed_amount)?; if !claimable_amount.is_zero() { let recipient = @@ -395,6 +411,7 @@ fn deregister_vesting_account( ])) } +/// Claim funds from the vesting accounts fn claim( deps: DepsMut, env: Env, @@ -468,6 +485,7 @@ fn claim( .add_attributes(attrs)) } +/// Build a send message for the given denom and amount fn build_send_msg( denom: Denom, amount: Uint128, @@ -587,7 +605,7 @@ fn vesting_account( Ok(VestingAccountResponse { address, vestings }) } -/// Allow the contract owner to withdraw the funds of the campaign +/// Allow the contract owner to withdraw the funds of the campaigngg /// /// Ensures the requested amount is available in the contract balance. Transfers /// tokens to the contract owner's account. @@ -646,12 +664,13 @@ pub fn withdraw( Ok(Response::new() .add_messages(vec![build_send_msg( campaign.denom, - amount, + min(amount, campaign.unallocated_amount), info.sender.to_string(), )?]) .add_attribute("withdraw", &amount.to_string()) .add_attribute( "campaign_unallocated_amount", &campaign.unallocated_amount.to_string(), - )) + ) + .add_attribute("is_campaign_inactive", &campaign.is_active.to_string())) } diff --git a/contracts/core-token-vesting/src/state.rs b/contracts/core-token-vesting/src/state.rs index c6065d3..6d1f378 100644 --- a/contracts/core-token-vesting/src/state.rs +++ b/contracts/core-token-vesting/src/state.rs @@ -28,9 +28,12 @@ pub struct Campaign { pub unallocated_amount: Uint128, pub denom: Denom, + pub owner: String, pub managers: Vec, + pub vesting_schedule: VestingSchedule, + pub is_active: bool, } From f2f71c101370f1e6c2711cd043708daa13d9b602 Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Thu, 8 Feb 2024 19:26:12 +0100 Subject: [PATCH 09/17] fix: remove campaign id and simplify reward logic" --- contracts/core-token-vesting/src/contract.rs | 206 +++++++----------- contracts/core-token-vesting/src/msg.rs | 19 +- contracts/core-token-vesting/src/state.rs | 6 +- .../tests/tests/test_airdrop.rs | 106 ++------- 4 files changed, 98 insertions(+), 239 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 25b7101..11efc87 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -19,8 +19,7 @@ use crate::msg::{ RewardUserResponse, VestingAccountResponse, VestingData, VestingSchedule, }; use crate::state::{ - denom_to_key, Campaign, VestingAccount, CAMPAIGN, USER_REWARDS, - VESTING_ACCOUNTS, + denom_to_key, Campaign, VestingAccount, CAMPAIGN, VESTING_ACCOUNTS, }; #[cfg_attr(not(feature = "library"), entry_point)] @@ -87,7 +86,6 @@ pub fn execute( } ExecuteMsg::CreateCampaign { vesting_schedule, - campaign_id, campaign_name, campaign_description, managers, @@ -96,25 +94,17 @@ pub fn execute( env, info, vesting_schedule, - campaign_id, campaign_name, campaign_description, managers, ), - ExecuteMsg::RewardUsers { - campaign_id, - requests, - } => reward_users(deps, env, info, campaign_id, requests), - ExecuteMsg::ClaimCampaign { campaign_id } => { - claim_campaign(deps, env, info, campaign_id) + ExecuteMsg::RewardUsers { requests } => { + reward_users(deps, env, info, requests) } - ExecuteMsg::DeactivateCampaign { campaign_id } => { - deactivate_campaign(deps, env, info, campaign_id) + ExecuteMsg::DeactivateCampaign {} => { + deactivate_campaign(deps, env, info) } - ExecuteMsg::Withdraw { - amount, - campaign_id, - } => withdraw(deps, env, info, amount, campaign_id.as_str()), + ExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount), } } @@ -125,10 +115,9 @@ fn deactivate_campaign( deps: DepsMut, env: Env, info: MessageInfo, - campaign_id: String, ) -> Result { let mut campaign = CAMPAIGN - .load(deps.storage, campaign_id.clone()) + .load(deps.storage) .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; if campaign.owner != info.sender @@ -144,55 +133,21 @@ fn deactivate_campaign( } campaign.is_active = false; - CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; - - let denom = to_string(&campaign.denom).unwrap(); - let own_balance: Uint128 = deps - .querier - .query_balance(&env.contract.address, denom) - .map_err(|_| StdError::generic_err("Failed to query contract balance"))? - .amount; - - return withdraw(deps, env, info, own_balance, campaign_id.as_str()); -} - -fn claim_campaign( - deps: DepsMut, - env: Env, - info: MessageInfo, - campaign_id: String, -) -> Result { - let user_reward = - USER_REWARDS - .load(deps.storage, info.sender.to_string()) - .map_err(|_| StdError::generic_err("Failed to load user rewards"))?; - - let campaign = CAMPAIGN - .load(deps.storage, campaign_id.clone()) - .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; + CAMPAIGN.save(deps.storage, &campaign)?; - register_vesting_account( - deps.storage, - env.block.time, - Some(campaign.owner), - info.sender.into_string(), - campaign.denom, - user_reward, - campaign.vesting_schedule, - ) + return withdraw(deps, env, info, campaign.unallocated_amount); } fn reward_users( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, - campaign_id: String, requests: Vec, ) -> Result { let mut res = vec![]; let mut campaign = CAMPAIGN - .load(deps.storage, campaign_id.clone()) + .load(deps.storage) .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; if campaign.owner != info.sender @@ -211,35 +166,55 @@ fn reward_users( StdError::generic_err("Insufficient funds for all rewards").into() ); } + + let master_address = Some(campaign.owner.clone()); + let mut attrs: Vec = vec![]; + for req in requests { - match USER_REWARDS.may_load(deps.storage, req.user_address.clone())? { - Some(mut user_reward) => { - user_reward += req.amount; - USER_REWARDS.save( - deps.storage, - req.user_address.clone(), - &user_reward, - )?; - } - None => { - USER_REWARDS.save( - deps.storage, - req.user_address.clone(), - &req.amount, - )?; - } - }; - res.push(RewardUserResponse { - user_address: req.user_address.clone(), - success: true, - error_msg: "".to_string(), - }); + // update the vesting amount inside the vesting schedule + let mut vesting_schedule = campaign.vesting_schedule.clone(); + + if let VestingSchedule::LinearVesting { + ref mut vesting_amount, + .. + } = vesting_schedule + { + *vesting_amount = req.amount; + } + + let result = register_vesting_account( + deps.storage, + env.block.time, + master_address.clone(), + req.user_address.clone(), + campaign.denom.clone(), + req.amount, + vesting_schedule, + ); + + if let Ok(response) = result { + attrs.extend(response.attributes); + res.push(RewardUserResponse { + user_address: req.user_address.clone(), + success: true, + error_msg: "".to_string(), + }); + } else { + let error = result.err().unwrap(); + res.push(RewardUserResponse { + user_address: req.user_address.clone(), + success: false, + error_msg: "Failed to register vesting account: ".to_string() + + &error.to_string(), + }); + } } campaign.unallocated_amount = campaign.unallocated_amount - total_requested; - CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; + CAMPAIGN.save(deps.storage, &campaign)?; Ok(Response::new() + .add_attributes(attrs) .add_attribute("method", "reward_users") .set_data(to_json_binary(&res).unwrap())) } @@ -249,15 +224,11 @@ fn create_campaign( _env: Env, info: MessageInfo, vesting_schedule: VestingSchedule, - campaign_id: String, campaign_name: String, campaign_description: String, managers: Vec, ) -> Result { - if CAMPAIGN - .may_load(deps.storage, campaign_id.clone())? - .is_some() - { + if CAMPAIGN.may_load(deps.storage)?.is_some() { return Err(StdError::generic_err("Campaign already exists").into()); } @@ -269,23 +240,22 @@ fn create_campaign( let campaign = Campaign { campaign_name: campaign_name, - campaign_id: campaign_id.clone(), campaign_description: campaign_description, owner: info.sender.into_string(), managers: managers, unallocated_amount: coin.amount, denom: Denom::Native(coin.denom.clone()), - vesting_schedule: vesting_schedule, + vesting_schedule: vesting_schedule.clone(), is_active: true, }; - CAMPAIGN.save(deps.storage, campaign_id.clone(), &campaign)?; + CAMPAIGN.save(deps.storage, &campaign)?; Ok(Response::new() .add_attribute("method", "create_campaign") - .add_attribute("campaign_id", campaign_id) .add_attribute("campaign_name", &campaign.campaign_name) .add_attribute("campaign_description", &campaign.campaign_description) - .add_attribute("initial_unallocated_amount", &coin.amount.to_string())) + .add_attribute("initial_unallocated_amount", &coin.amount.to_string()) + .add_attribute("schedule", &to_string(&vesting_schedule).unwrap())) } fn register_vesting_account( @@ -311,7 +281,7 @@ fn register_vesting_account( address: address.to_string(), vesting_denom: deposit_denom.clone(), vesting_amount: deposit_amount, - vesting_schedule, + vesting_schedule: vesting_schedule, claimed_amount: Uint128::zero(), }; @@ -605,66 +575,38 @@ fn vesting_account( Ok(VestingAccountResponse { address, vestings }) } -/// Allow the contract owner to withdraw the funds of the campaigngg +/// Allow the contract owner to withdraw the funds of the campaign /// -/// Ensures the requested amount is available in the contract balance. Transfers -/// tokens to the contract owner's account. +/// Ensures the requested amount is available in the contract balance. +/// Ensures the requested amount is less than or equal to the unallocated amount pub fn withdraw( deps: DepsMut, _env: Env, info: MessageInfo, amount: Uint128, - campaign_id: &str, ) -> Result { - let campaign = CAMPAIGN.load(deps.storage, campaign_id.to_string())?; + let campaign = CAMPAIGN.load(deps.storage)?; if info.sender != campaign.owner { return Err( - StdError::generic_err("Only contract owner can withdraw").into() + StdError::generic_err("Only campaign owner can withdraw").into() ); } - // Update campaign unallocated amount - if amount > campaign.unallocated_amount { - let update_result = CAMPAIGN.update( - deps.storage, - campaign_id.to_string(), - |campaign| -> StdResult { - if let Some(mut campaign) = campaign { - campaign.unallocated_amount = Uint128::zero(); - Ok(campaign) - } else { - Err(StdError::generic_err("Campaign not found")) - } - }, - ); - - if let Err(e) = update_result { - return Err(e.into()); - } - } else { - let update_result = CAMPAIGN.update( - deps.storage, - campaign_id.to_string(), - |campaign| -> StdResult { - if let Some(mut campaign) = campaign { - campaign.unallocated_amount -= amount; - Ok(campaign) - } else { - Err(StdError::generic_err("Campaign not found")) - } - }, - ); - - if let Err(e) = update_result { - return Err(e.into()); - } + let amount_max = min(amount, campaign.unallocated_amount); + if amount_max.is_zero() { + return Err(StdError::generic_err("Nothing to withdraw").into()); } + CAMPAIGN.update(deps.storage, |mut campaign| -> StdResult { + campaign.unallocated_amount = campaign.unallocated_amount - amount_max; + Ok(campaign) + })?; + Ok(Response::new() .add_messages(vec![build_send_msg( campaign.denom, - min(amount, campaign.unallocated_amount), + amount_max, info.sender.to_string(), )?]) .add_attribute("withdraw", &amount.to_string()) diff --git a/contracts/core-token-vesting/src/msg.rs b/contracts/core-token-vesting/src/msg.rs index cd5dab2..4f06e10 100644 --- a/contracts/core-token-vesting/src/msg.rs +++ b/contracts/core-token-vesting/src/msg.rs @@ -50,14 +50,12 @@ pub enum ExecuteMsg { /// Create campaign to reward users with vested tokens /// Args: /// - vesting_schedule: VestingSchedule: The vesting schedule of the account. - /// - campaign_id: String: The unique identifier of the campaign. /// - campaign_name: String: The name of the campaign. /// - campaign_description: String: The description of the campaign. /// - managers: Vec: The list of addresses that can manage the campaign (reward users). CreateCampaign { vesting_schedule: VestingSchedule, - campaign_id: String, campaign_name: String, campaign_description: String, managers: Vec, @@ -65,35 +63,20 @@ pub enum ExecuteMsg { /// Reward users with tokens /// Args: - /// - campaign_id: String: The unique identifier of the campaign. /// - requests: Vec: The list of reward requests. RewardUsers { - campaign_id: String, requests: Vec, }, - /// Claim campaign: A user can claim vested tokens from a campaign and this - /// will register a vesting account for the user. - /// Args: - /// - campaign_id: String: The unique identifier of the campaign. - ClaimCampaign { - campaign_id: String, - }, - /// Deactivate campaign: The campaign owner can deactivate the campaign. /// All the unallocated tokens will be returned to the owner. /// Args: - /// - campaign_id: String: The unique identifier of the campaign. - DeactivateCampaign { - campaign_id: String, - }, + DeactivateCampaign {}, /// Withdraw: The campaign owner can withdraw unallocated tokens from the campaign. /// Args: - /// - campaign_id: String: The unique identifier of the campaign. /// - amount: Uint128: The amount of tokens to be withdrawn. Withdraw { - campaign_id: String, amount: Uint128, }, } diff --git a/contracts/core-token-vesting/src/state.rs b/contracts/core-token-vesting/src/state.rs index 6d1f378..5e225de 100644 --- a/contracts/core-token-vesting/src/state.rs +++ b/contracts/core-token-vesting/src/state.rs @@ -3,10 +3,9 @@ use cosmwasm_schema::cw_serde; use crate::msg::VestingSchedule; use cosmwasm_std::{CosmosMsg, Uint128}; use cw20::Denom; -use cw_storage_plus::Map; +use cw_storage_plus::{Item, Map}; -pub const CAMPAIGN: Map = Map::new("campaign"); -pub const USER_REWARDS: Map = Map::new("user_rewards"); +pub const CAMPAIGN: Item = Item::new("campaign"); pub const VESTING_ACCOUNTS: Map<(&str, &str), VestingAccount> = Map::new("vesting_accounts"); @@ -22,7 +21,6 @@ pub struct VestingAccount { #[cw_serde] pub struct Campaign { - pub campaign_id: String, pub campaign_name: String, pub campaign_description: String, diff --git a/contracts/core-token-vesting/tests/tests/test_airdrop.rs b/contracts/core-token-vesting/tests/tests/test_airdrop.rs index 83cd88f..bda04ab 100644 --- a/contracts/core-token-vesting/tests/tests/test_airdrop.rs +++ b/contracts/core-token-vesting/tests/tests/test_airdrop.rs @@ -9,7 +9,7 @@ use token_vesting::{ contract::execute, errors::ContractError, msg::{ExecuteMsg, RewardUserRequest, VestingSchedule}, - state::{CAMPAIGN, USER_REWARDS}, + state::{denom_to_key, CAMPAIGN, VESTING_ACCOUNTS}, }; use super::{helpers::TestResult, test_manager::setup_with_block_time}; @@ -25,7 +25,7 @@ fn execute_create_campaign_valid() -> TestResult { end_time: Uint64::new(200), vesting_amount: Uint128::new(5000), }, - campaign_id: "campaign1".to_string(), + campaign_name: "Test Campaign".to_string(), campaign_description: "A test campaign".to_string(), managers: vec!["manager1".to_string(), "manager2".to_string()], @@ -45,7 +45,7 @@ fn execute_create_campaign_valid() -> TestResult { "Expected 'create_campaign' method in response attributes" ); assert!( - CAMPAIGN.has(&deps.storage, "campaign1".to_string()), + CAMPAIGN.may_load(deps.as_ref().storage)?.is_some(), "Campaign should be saved in state" ); @@ -57,9 +57,7 @@ fn execute_create_campaign_duplicate_id() -> TestResult { let (mut deps, _env) = setup_with_block_time(0)?; // Create a campaign with a unique ID - let campaign_id = "unique_campaign_id"; let create_campaign_msg = ExecuteMsg::CreateCampaign { - campaign_id: campaign_id.to_string(), campaign_name: "Test Campaign".to_string(), campaign_description: "This is a test campaign".to_string(), managers: vec![], @@ -109,7 +107,6 @@ fn execute_create_campaign_invalid_coin_count() -> TestResult { end_time: Uint64::new(200), vesting_amount: Uint128::new(5000), }, - campaign_id: "campaign1".to_string(), campaign_name: "Test Campaign".to_string(), campaign_description: "A test campaign".to_string(), managers: vec!["manager1".to_string(), "manager2".to_string()], @@ -145,7 +142,6 @@ fn execute_create_campaign_2_coins() -> TestResult { end_time: Uint64::new(200), vesting_amount: Uint128::new(5000), }, - campaign_id: "campaign2".to_string(), campaign_name: "Test Campaign".to_string(), campaign_description: "A test campaign".to_string(), managers: vec!["manager1".to_string(), "manager2".to_string()], @@ -175,13 +171,11 @@ fn execute_reward_users_unactive_campaign() -> TestResult { let (mut deps, env) = setup_with_block_time(0)?; // Create a campaign - let campaign_id = "campaign1".to_string(); execute( deps.as_mut(), env.clone(), mock_info("creator", &[coin(10000, "token")]), ExecuteMsg::CreateCampaign { - campaign_id: campaign_id.clone(), campaign_name: "Campaign One".to_string(), campaign_description: "The first campaign".to_string(), managers: vec!["manager1".to_string()], @@ -194,15 +188,12 @@ fn execute_reward_users_unactive_campaign() -> TestResult { )?; // Deactivate the campaign - let msg = ExecuteMsg::DeactivateCampaign { - campaign_id: campaign_id.clone(), - }; + let msg = ExecuteMsg::DeactivateCampaign {}; let info = mock_info("creator", &[]); execute(deps.as_mut(), env.clone(), info, msg)?; // Reward users let reward_users_msg = ExecuteMsg::RewardUsers { - campaign_id: campaign_id.clone(), requests: vec![ RewardUserRequest { user_address: "user1".to_string(), @@ -239,13 +230,11 @@ fn execute_reward_users_unauthorized() -> TestResult { let (mut deps, env) = setup_with_block_time(0)?; // Create a campaign - let campaign_id = "campaign1".to_string(); execute( deps.as_mut(), env.clone(), mock_info("creator", &[coin(10000, "token")]), ExecuteMsg::CreateCampaign { - campaign_id: campaign_id.clone(), campaign_name: "Campaign One".to_string(), campaign_description: "The first campaign".to_string(), managers: vec!["manager1".to_string()], @@ -259,7 +248,6 @@ fn execute_reward_users_unauthorized() -> TestResult { // Reward users let reward_users_msg = ExecuteMsg::RewardUsers { - campaign_id: campaign_id.clone(), requests: vec![ RewardUserRequest { user_address: "user1".to_string(), @@ -293,13 +281,11 @@ fn execute_reward_users_valid() -> TestResult { let (mut deps, env) = setup_with_block_time(0)?; // Create a campaign - let campaign_id = "campaign1".to_string(); execute( deps.as_mut(), env.clone(), mock_info("creator", &[coin(10000, "token")]), ExecuteMsg::CreateCampaign { - campaign_id: campaign_id.clone(), campaign_name: "Campaign One".to_string(), campaign_description: "The first campaign".to_string(), managers: vec!["manager1".to_string()], @@ -313,7 +299,6 @@ fn execute_reward_users_valid() -> TestResult { // Reward users let reward_users_msg = ExecuteMsg::RewardUsers { - campaign_id: campaign_id.clone(), requests: vec![ RewardUserRequest { user_address: "user1".to_string(), @@ -325,6 +310,7 @@ fn execute_reward_users_valid() -> TestResult { }, ], }; + execute( deps.as_mut(), env.clone(), @@ -332,53 +318,18 @@ fn execute_reward_users_valid() -> TestResult { reward_users_msg, )?; - // Verify user rewards and campaign state - let user1_rewards = - USER_REWARDS.load(deps.as_ref().storage, "user1".to_string())?; - assert_eq!( - user1_rewards, - Uint128::new(500), - "User1 rewards do not match." - ); - - let user2_rewards = - USER_REWARDS.load(deps.as_ref().storage, "user2".to_string())?; - assert_eq!( - user2_rewards, - Uint128::new(1500), - "User2 rewards do not match." - ); - - let updated_campaign = - CAMPAIGN.load(deps.as_ref().storage, campaign_id.clone())?; - assert_eq!( - updated_campaign.unallocated_amount, - Uint128::new(8000), - "Campaign unallocated amount does not match expected." - ); + // Assert there's a vesting account for each user + let campaign = CAMPAIGN.load(deps.as_ref().storage)?; - // Additional reward - let reward_users_msg = ExecuteMsg::RewardUsers { - campaign_id: campaign_id.clone(), - requests: vec![RewardUserRequest { - user_address: "user1".to_string(), - amount: Uint128::new(1000), - }], - }; - execute( - deps.as_mut(), - env, - mock_info("creator", &[]), - reward_users_msg, + let vesting_account = VESTING_ACCOUNTS.load( + deps.as_ref().storage, + ("user1", &denom_to_key(campaign.denom.clone())), )?; - // Verify user rewards and campaign state - let user1_rewards = - USER_REWARDS.load(deps.as_ref().storage, "user1".to_string())?; assert_eq!( - user1_rewards, - Uint128::new(1500), - "User1 rewards do not match." + vesting_account.vesting_amount, + Uint128::new(500), + "Vesting amount not set correctly for user1" ); Ok(()) @@ -389,13 +340,11 @@ fn execute_reward_users_insufficient_funds() -> TestResult { let (mut deps, _env) = setup_with_block_time(0)?; // Create a campaign with limited funds - let campaign_id = "limited_fund_campaign"; execute( deps.as_mut(), mock_env(), mock_info("creator", &[coin(500, "token")]), ExecuteMsg::CreateCampaign { - campaign_id: campaign_id.to_string(), campaign_name: "Limited Fund Campaign".to_string(), campaign_description: "This campaign has limited funds".to_string(), managers: vec![], @@ -413,7 +362,6 @@ fn execute_reward_users_insufficient_funds() -> TestResult { mock_env(), mock_info("creator", &[]), ExecuteMsg::RewardUsers { - campaign_id: campaign_id.to_string(), requests: vec![RewardUserRequest { user_address: "user1".to_string(), amount: Uint128::new(600), // More than available @@ -472,7 +420,6 @@ fn execute_withdraw_valid() -> TestResult { end_time: Uint64::new(env.block.time.seconds() + 100), vesting_amount: Uint128::new(1000), }, - campaign_id: "campaign1".to_string(), campaign_name: "Test Campaign".to_string(), campaign_description: "A campaign for testing".to_string(), managers: vec!["manager1".to_string()], @@ -489,15 +436,12 @@ fn execute_withdraw_valid() -> TestResult { // Attempt to withdraw unallocated funds let withdraw_msg = ExecuteMsg::Withdraw { amount: Uint128::new(500), - campaign_id: "campaign1".to_string(), }; let info = mock_info("owner", &[]); execute(deps.as_mut(), env.clone(), info, withdraw_msg)?; // Verify campaign unallocated amount is updated - let campaign = CAMPAIGN - .load(&deps.storage, "campaign1".to_string()) - .unwrap(); + let campaign = CAMPAIGN.load(&deps.storage).unwrap(); assert_eq!( campaign.unallocated_amount, Uint128::new(500), @@ -512,19 +456,18 @@ fn execute_withdraw_unauthorized() -> TestResult { let (mut deps, env) = setup_with_block_time(100)?; // Create a campaign with some funds - create_test_campaign(&mut deps, &env, "campaign1", "owner"); + create_test_campaign(&mut deps, &env, "owner"); // Attempt to withdraw funds from the contract by an unauthorized user let msg = ExecuteMsg::Withdraw { amount: Uint128::new(500), - campaign_id: "campaign1".to_string(), }; let info = mock_info("unauthorized_user", &[]); let res = execute(deps.as_mut(), env.clone(), info, msg); match res { Err(ContractError::Std(StdError::GenericErr { msg, .. })) - if msg.contains("Only contract owner can withdraw") => + if msg.contains("Only campaign owner can withdraw") => { Ok(()) } @@ -537,18 +480,15 @@ fn execute_deactivate_campaign_authorized() -> TestResult { let (mut deps, env) = setup_with_block_time(200)?; // Create a campaign and mark it active - create_test_campaign(&mut deps, &env, "campaign2", "owner"); + create_test_campaign(&mut deps, &env, "owner"); // Deactivate the campaign by the owner - let msg = ExecuteMsg::DeactivateCampaign { - campaign_id: "campaign2".to_string(), - }; + let msg = ExecuteMsg::DeactivateCampaign {}; let info = mock_info("owner", &[]); execute(deps.as_mut(), env.clone(), info, msg)?; // Check if the campaign is deactivated - let campaign = - CAMPAIGN.load(deps.as_ref().storage, "campaign2".to_string())?; + let campaign = CAMPAIGN.load(deps.as_ref().storage)?; assert_eq!(campaign.is_active, false, "Campaign should be deactivated"); Ok(()) @@ -559,12 +499,10 @@ fn execute_deactivate_campaign_unauthorized() -> TestResult { let (mut deps, env) = setup_with_block_time(300)?; // Create a campaign and mark it active - create_test_campaign(&mut deps, &env, "campaign3", "owner"); + create_test_campaign(&mut deps, &env, "owner"); // Attempt to deactivate the campaign by an unauthorized user - let msg = ExecuteMsg::DeactivateCampaign { - campaign_id: "campaign3".to_string(), - }; + let msg = ExecuteMsg::DeactivateCampaign {}; let info = mock_info("unauthorized_user", &[]); let res = execute(deps.as_mut(), env, info, msg); @@ -584,7 +522,6 @@ fn execute_deactivate_campaign_unauthorized() -> TestResult { fn create_test_campaign( deps: &mut OwnedDeps, env: &Env, - campaign_id: &str, owner: &str, ) { let msg = ExecuteMsg::CreateCampaign { @@ -593,7 +530,6 @@ fn create_test_campaign( end_time: Uint64::new(env.block.time.seconds() + 200), vesting_amount: Uint128::new(1000), }, - campaign_id: campaign_id.to_string(), campaign_name: "Test Campaign".to_string(), campaign_description: "A campaign for testing".to_string(), managers: vec![owner.to_string()], From 258a3e76c48926e668314a08b1c43cc5755cd3c0 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Mon, 19 Feb 2024 15:39:02 +0100 Subject: [PATCH 10/17] remove unused param --- .gitignore | 1 + contracts/core-token-vesting/src/contract.rs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index aad66ca..bab7ff1 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ dist node_modules/ yarn.lock .vscode/ +.idea/ contracts/*/.editorconfig packages/*/.editorconfig diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 11efc87..ca1daf2 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -91,7 +91,6 @@ pub fn execute( managers, } => create_campaign( deps, - env, info, vesting_schedule, campaign_name, @@ -221,7 +220,6 @@ fn reward_users( fn create_campaign( deps: DepsMut, - _env: Env, info: MessageInfo, vesting_schedule: VestingSchedule, campaign_name: String, From 7d2e3c8f7f8c81008b18e827bf5d611b00228442 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Mon, 19 Feb 2024 18:01:05 +0100 Subject: [PATCH 11/17] remove unused value --- contracts/core-token-vesting/src/contract.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index ca1daf2..a5b31e9 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -484,7 +484,6 @@ pub fn receive_cw20( cw20_msg: Cw20ReceiveMsg, ) -> Result { let amount = cw20_msg.amount; - let _sender = cw20_msg.sender; let contract = info.sender; match from_json(&cw20_msg.msg) { From e80f7db3affbac47899a05de8b0bfe4460f33568 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Tue, 20 Feb 2024 14:33:32 +0100 Subject: [PATCH 12/17] avoid double clone --- contracts/core-token-vesting/src/contract.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index a5b31e9..49d3fa6 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -166,7 +166,6 @@ fn reward_users( ); } - let master_address = Some(campaign.owner.clone()); let mut attrs: Vec = vec![]; for req in requests { @@ -184,7 +183,7 @@ fn reward_users( let result = register_vesting_account( deps.storage, env.block.time, - master_address.clone(), + Some(campaign.owner.clone()), req.user_address.clone(), campaign.denom.clone(), req.amount, @@ -237,10 +236,10 @@ fn create_campaign( let coin = info.funds.get(0).unwrap(); let campaign = Campaign { - campaign_name: campaign_name, - campaign_description: campaign_description, + campaign_name, + campaign_description, owner: info.sender.into_string(), - managers: managers, + managers, unallocated_amount: coin.amount, denom: Denom::Native(coin.denom.clone()), vesting_schedule: vesting_schedule.clone(), From aaf9dc1db2d3847f92037f0b74f55ae5b20d3801 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Tue, 20 Feb 2024 16:12:02 +0100 Subject: [PATCH 13/17] clean code --- contracts/core-token-vesting/src/contract.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index a5b31e9..904158f 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -234,13 +234,13 @@ fn create_campaign( return Err(StdError::generic_err("one denom sent required").into()); } - let coin = info.funds.get(0).unwrap(); + let coin = info.funds.get(0)?; let campaign = Campaign { - campaign_name: campaign_name, - campaign_description: campaign_description, + campaign_name, + campaign_description, owner: info.sender.into_string(), - managers: managers, + managers, unallocated_amount: coin.amount, denom: Denom::Native(coin.denom.clone()), vesting_schedule: vesting_schedule.clone(), @@ -279,7 +279,7 @@ fn register_vesting_account( address: address.to_string(), vesting_denom: deposit_denom.clone(), vesting_amount: deposit_amount, - vesting_schedule: vesting_schedule, + vesting_schedule, claimed_amount: Uint128::zero(), }; From 346ae7a0ef784ab687bdaffeac634ed1091f36b2 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Tue, 20 Feb 2024 16:18:26 +0100 Subject: [PATCH 14/17] add more control over erroring in info.funds.get --- contracts/core-token-vesting/src/contract.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 6245454..b6d7b3c 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -233,7 +233,9 @@ fn create_campaign( return Err(StdError::generic_err("one denom sent required").into()); } - let coin = info.funds.get(0)?; + let coin = info.funds.get(0).ok_or(StdError::generic_err( + "one denom sent required, unexpected error", + ))?; let campaign = Campaign { campaign_name, From 3df717102869608aed2d89026ec4e42937d6b31d Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Tue, 20 Feb 2024 16:47:40 +0100 Subject: [PATCH 15/17] remove clone instances --- contracts/core-token-vesting/src/contract.rs | 6 +++--- contracts/core-token-vesting/src/state.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index b6d7b3c..f8707a6 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -266,7 +266,7 @@ fn register_vesting_account( deposit_amount: Uint128, vesting_schedule: VestingSchedule, ) -> Result { - let denom_key = denom_to_key(deposit_denom.clone()); + let denom_key = denom_to_key(&deposit_denom); // vesting_account existence check if VESTING_ACCOUNTS.has(storage, (address.as_str(), &denom_key)) { @@ -311,7 +311,7 @@ fn deregister_vesting_account( vested_token_recipient: Option, left_vesting_token_recipient: Option, ) -> Result { - let denom_key = denom_to_key(denom.clone()); + let denom_key = denom_to_key(&denom); let sender = info.sender; let mut messages: Vec = vec![]; @@ -394,7 +394,7 @@ fn claim( let mut messages: Vec = vec![]; let mut attrs: Vec = vec![]; for denom in denoms.iter() { - let denom_key = denom_to_key(denom.clone()); + let denom_key = denom_to_key(&denom); // vesting_account existence check let account = VESTING_ACCOUNTS diff --git a/contracts/core-token-vesting/src/state.rs b/contracts/core-token-vesting/src/state.rs index 5e225de..4330dbc 100644 --- a/contracts/core-token-vesting/src/state.rs +++ b/contracts/core-token-vesting/src/state.rs @@ -40,7 +40,7 @@ pub struct DeregisterResult<'a> { pub attributes: Vec<(&'a str, String)>, } -pub fn denom_to_key(denom: Denom) -> String { +pub fn denom_to_key(denom: &Denom) -> String { match denom { Denom::Cw20(addr) => format!("cw20-{}", addr), Denom::Native(denom) => format!("native-{}", denom), From 9d69628df8b9d16023ea25bbffaccdaf4e36e9bd Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Wed, 21 Feb 2024 10:33:06 +0100 Subject: [PATCH 16/17] fix: make sure addresses are valid when adding managers --- contracts/core-token-vesting/src/contract.rs | 14 ++++--- contracts/core-token-vesting/src/state.rs | 2 +- .../tests/tests/test_airdrop.rs | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index f8707a6..2432f65 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -119,9 +119,7 @@ fn deactivate_campaign( .load(deps.storage) .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; - if campaign.owner != info.sender - && !campaign.managers.contains(&info.sender.to_string()) - { + if campaign.owner != info.sender { return Err(StdError::generic_err("Unauthorized. Only the campaign owner or managers can deactivate the campaign").into()); } @@ -237,6 +235,10 @@ fn create_campaign( "one denom sent required, unexpected error", ))?; + for manager in managers.iter() { + let _ = deps.api.addr_validate(manager)?; + } + let campaign = Campaign { campaign_name, campaign_description, @@ -266,7 +268,7 @@ fn register_vesting_account( deposit_amount: Uint128, vesting_schedule: VestingSchedule, ) -> Result { - let denom_key = denom_to_key(&deposit_denom); + let denom_key = denom_to_key(deposit_denom.clone()); // vesting_account existence check if VESTING_ACCOUNTS.has(storage, (address.as_str(), &denom_key)) { @@ -311,7 +313,7 @@ fn deregister_vesting_account( vested_token_recipient: Option, left_vesting_token_recipient: Option, ) -> Result { - let denom_key = denom_to_key(&denom); + let denom_key = denom_to_key(denom.clone()); let sender = info.sender; let mut messages: Vec = vec![]; @@ -394,7 +396,7 @@ fn claim( let mut messages: Vec = vec![]; let mut attrs: Vec = vec![]; for denom in denoms.iter() { - let denom_key = denom_to_key(&denom); + let denom_key = denom_to_key(denom.clone()); // vesting_account existence check let account = VESTING_ACCOUNTS diff --git a/contracts/core-token-vesting/src/state.rs b/contracts/core-token-vesting/src/state.rs index 4330dbc..5e225de 100644 --- a/contracts/core-token-vesting/src/state.rs +++ b/contracts/core-token-vesting/src/state.rs @@ -40,7 +40,7 @@ pub struct DeregisterResult<'a> { pub attributes: Vec<(&'a str, String)>, } -pub fn denom_to_key(denom: &Denom) -> String { +pub fn denom_to_key(denom: Denom) -> String { match denom { Denom::Cw20(addr) => format!("cw20-{}", addr), Denom::Native(denom) => format!("native-{}", denom), diff --git a/contracts/core-token-vesting/tests/tests/test_airdrop.rs b/contracts/core-token-vesting/tests/tests/test_airdrop.rs index bda04ab..62153d1 100644 --- a/contracts/core-token-vesting/tests/tests/test_airdrop.rs +++ b/contracts/core-token-vesting/tests/tests/test_airdrop.rs @@ -52,6 +52,43 @@ fn execute_create_campaign_valid() -> TestResult { Ok(()) } +#[test] +fn execute_create_campaign_invalid_manager() -> TestResult { + let (mut deps, env) = setup_with_block_time(0)?; + + // Create a campaign with valid parameters + let create_campaign_msg = ExecuteMsg::CreateCampaign { + vesting_schedule: VestingSchedule::LinearVesting { + start_time: Uint64::new(100), + end_time: Uint64::new(200), + vesting_amount: Uint128::new(5000), + }, + + campaign_name: "Test Campaign".to_string(), + campaign_description: "A test campaign".to_string(), + managers: vec!["".to_string(), "manager2".to_string()], + }; + let res = execute( + deps.as_mut(), + env, + mock_info("creator", &[coin(5000, "token")]), + create_campaign_msg, + ); + + // Assertions that res has error with "human address too short for this mock implementation" + match res { + Err(ContractError::Std(StdError::GenericErr { msg, .. })) + if msg.contains("human address too short for this mock implementation") => + { + Ok(()) + } + _ => Err(anyhow!( + "Expected 'human address too short for this mock implementation' error, found {:?}", + res + )), + } +} + #[test] fn execute_create_campaign_duplicate_id() -> TestResult { let (mut deps, _env) = setup_with_block_time(0)?; From 69f12f1f680c8e9e436b9ca353cfa2046521b53f Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Wed, 21 Feb 2024 10:34:17 +0100 Subject: [PATCH 17/17] fix: add comment --- contracts/core-token-vesting/src/contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/core-token-vesting/src/contract.rs b/contracts/core-token-vesting/src/contract.rs index 2432f65..f053a8a 100644 --- a/contracts/core-token-vesting/src/contract.rs +++ b/contracts/core-token-vesting/src/contract.rs @@ -235,6 +235,7 @@ fn create_campaign( "one denom sent required, unexpected error", ))?; + // validate managers for manager in managers.iter() { let _ = deps.api.addr_validate(manager)?; }