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/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/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 a73c965..f053a8a 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::{ @@ -13,10 +15,12 @@ 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, VestingAccount, CAMPAIGN, VESTING_ACCOUNTS, }; -use crate::state::{denom_to_key, VestingAccount, VESTING_ACCOUNTS}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -80,9 +84,182 @@ pub fn execute( ExecuteMsg::Claim { denoms, recipient } => { claim(deps, env, info, denoms, recipient) } + ExecuteMsg::CreateCampaign { + vesting_schedule, + campaign_name, + campaign_description, + managers, + } => create_campaign( + deps, + info, + vesting_schedule, + campaign_name, + campaign_description, + managers, + ), + ExecuteMsg::RewardUsers { requests } => { + reward_users(deps, env, info, requests) + } + ExecuteMsg::DeactivateCampaign {} => { + deactivate_campaign(deps, env, info) + } + ExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount), } } +/// 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, + info: MessageInfo, +) -> Result { + let mut campaign = CAMPAIGN + .load(deps.storage) + .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; + + if campaign.owner != info.sender { + return Err(StdError::generic_err("Unauthorized. Only the campaign owner or managers can deactivate the campaign").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)?; + + return withdraw(deps, env, info, campaign.unallocated_amount); +} + +fn reward_users( + deps: DepsMut, + env: Env, + info: MessageInfo, + requests: Vec, +) -> Result { + let mut res = vec![]; + + let mut campaign = CAMPAIGN + .load(deps.storage) + .map_err(|_| StdError::generic_err("Failed to load campaign data"))?; + + if campaign.owner != info.sender + && !campaign.managers.contains(&info.sender.into()) + { + return Err(StdError::generic_err("Unauthorized").into()); + } + + if !campaign.is_active { + return Err(StdError::generic_err("Campaign is not active").into()); + } + + 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() + ); + } + + let mut attrs: Vec = vec![]; + + for req in requests { + // 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, + Some(campaign.owner.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)?; + + Ok(Response::new() + .add_attributes(attrs) + .add_attribute("method", "reward_users") + .set_data(to_json_binary(&res).unwrap())) +} + +fn create_campaign( + deps: DepsMut, + info: MessageInfo, + vesting_schedule: VestingSchedule, + campaign_name: String, + campaign_description: String, + managers: Vec, +) -> Result { + if CAMPAIGN.may_load(deps.storage)?.is_some() { + return Err(StdError::generic_err("Campaign already exists").into()); + } + + if info.funds.len() != 1 { + return Err(StdError::generic_err("one denom sent required").into()); + } + + let coin = info.funds.get(0).ok_or(StdError::generic_err( + "one denom sent required, unexpected error", + ))?; + + // validate managers + for manager in managers.iter() { + let _ = deps.api.addr_validate(manager)?; + } + + let campaign = Campaign { + campaign_name, + campaign_description, + owner: info.sender.into_string(), + managers, + unallocated_amount: coin.amount, + denom: Denom::Native(coin.denom.clone()), + vesting_schedule: vesting_schedule.clone(), + is_active: true, + }; + CAMPAIGN.save(deps.storage, &campaign)?; + + Ok(Response::new() + .add_attribute("method", "create_campaign") + .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("schedule", &to_string(&vesting_schedule).unwrap())) +} + fn register_vesting_account( storage: &mut dyn Storage, block_time: Timestamp, @@ -101,18 +278,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![ @@ -205,6 +383,7 @@ fn deregister_vesting_account( ])) } +/// Claim funds from the vesting accounts fn claim( deps: DepsMut, env: Env, @@ -278,6 +457,7 @@ fn claim( .add_attributes(attrs)) } +/// Build a send message for the given denom and amount fn build_send_msg( denom: Denom, amount: Uint128, @@ -308,7 +488,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) { @@ -397,152 +576,44 @@ fn vesting_account( Ok(VestingAccountResponse { address, vestings }) } -#[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)) - } +/// Allow the contract owner to withdraw the funds of the campaign +/// +/// 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, +) -> Result { + let campaign = CAMPAIGN.load(deps.storage)?; - #[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, + if info.sender != campaign.owner { + return Err( + StdError::generic_err("Only campaign owner can withdraw").into() ); - - 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(()) + let amount_max = min(amount, campaign.unallocated_amount); + if amount_max.is_zero() { + return Err(StdError::generic_err("Nothing to withdraw").into()); } - #[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, - )?; + CAMPAIGN.update(deps.storage, |mut campaign| -> StdResult { + campaign.unallocated_amount = campaign.unallocated_amount - amount_max; + Ok(campaign) + })?; - Ok(()) - } + Ok(Response::new() + .add_messages(vec![build_send_msg( + campaign.denom, + amount_max, + 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/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/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 ca496fb..4f06e10 100644 --- a/contracts/core-token-vesting/src/msg.rs +++ b/contracts/core-token-vesting/src/msg.rs @@ -46,6 +46,52 @@ 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_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_name: String, + campaign_description: String, + managers: Vec, + }, + + /// Reward users with tokens + /// Args: + /// - requests: Vec: The list of reward requests. + RewardUsers { + requests: Vec, + }, + + /// Deactivate campaign: The campaign owner can deactivate the campaign. + /// All the unallocated tokens will be returned to the owner. + /// Args: + DeactivateCampaign {}, + + /// Withdraw: The campaign owner can withdraw unallocated tokens from the campaign. + /// Args: + /// - amount: Uint128: The amount of tokens to be withdrawn. + Withdraw { + 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] @@ -258,44 +304,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/src/state.rs b/contracts/core-token-vesting/src/state.rs index 729586a..5e225de 100644 --- a/contracts/core-token-vesting/src/state.rs +++ b/contracts/core-token-vesting/src/state.rs @@ -1,10 +1,11 @@ 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; +use cw_storage_plus::{Item, Map}; +pub const CAMPAIGN: Item = Item::new("campaign"); pub const VESTING_ACCOUNTS: Map<(&str, &str), VestingAccount> = Map::new("vesting_accounts"); @@ -18,6 +19,27 @@ pub struct VestingAccount { pub claimed_amount: Uint128, } +#[cw_serde] +pub struct Campaign { + pub campaign_name: String, + pub campaign_description: String, + + pub unallocated_amount: Uint128, + pub denom: Denom, + + pub owner: String, + pub managers: Vec, + + pub vesting_schedule: VestingSchedule, + + 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), 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..62153d1 --- /dev/null +++ b/contracts/core-token-vesting/tests/tests/test_airdrop.rs @@ -0,0 +1,576 @@ +use anyhow::anyhow; +use cosmwasm_std::{ + coin, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + 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, VESTING_ACCOUNTS}, +}; + +use super::{helpers::TestResult, test_manager::setup_with_block_time}; + +#[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_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.may_load(deps.as_ref().storage)?.is_some(), + "Campaign should be saved in state" + ); + + 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)?; + + // Create a campaign with a unique ID + let create_campaign_msg = ExecuteMsg::CreateCampaign { + 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", &[coin(5000, "token")]), + create_campaign_msg.clone(), + )?; + + // Attempt to create another campaign with the same ID + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("creator", &[coin(5000, "token")]), + 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_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_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_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 + execute( + deps.as_mut(), + env.clone(), + mock_info("creator", &[coin(10000, "token")]), + ExecuteMsg::CreateCampaign { + 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), + }, + }, + )?; + + // Deactivate the campaign + 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 { + 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("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 + execute( + deps.as_mut(), + env.clone(), + mock_info("creator", &[coin(10000, "token")]), + ExecuteMsg::CreateCampaign { + 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 { + 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 + execute( + deps.as_mut(), + env.clone(), + mock_info("creator", &[coin(10000, "token")]), + ExecuteMsg::CreateCampaign { + 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 { + 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, + )?; + + // Assert there's a vesting account for each user + let campaign = CAMPAIGN.load(deps.as_ref().storage)?; + + let vesting_account = VESTING_ACCOUNTS.load( + deps.as_ref().storage, + ("user1", &denom_to_key(campaign.denom.clone())), + )?; + + assert_eq!( + vesting_account.vesting_amount, + Uint128::new(500), + "Vesting amount not set correctly for user1" + ); + + Ok(()) +} + +#[test] +fn execute_reward_users_insufficient_funds() -> TestResult { + let (mut deps, _env) = setup_with_block_time(0)?; + + // Create a campaign with limited funds + execute( + deps.as_mut(), + mock_env(), + mock_info("creator", &[coin(500, "token")]), + ExecuteMsg::CreateCampaign { + 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 { + 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("Insufficient funds for all rewards") => + { + Ok(()) + } + _ => Err(anyhow!( + "Expected 'Insufficient funds for all rewards' error, found {:?}", + res + )), + } +} + +#[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_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, "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), + }; + 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).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, "owner"); + + // Attempt to withdraw funds from the contract by an unauthorized user + let msg = ExecuteMsg::Withdraw { + amount: Uint128::new(500), + }; + 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 campaign 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, "owner"); + + // Deactivate the campaign by the owner + 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)?; + 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, "owner"); + + // Attempt to deactivate the campaign by an unauthorized user + let msg = ExecuteMsg::DeactivateCampaign {}; + 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, + 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_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..c274533 --- /dev/null +++ b/contracts/core-token-vesting/tests/tests/test_manager.rs @@ -0,0 +1,189 @@ +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(()) +} + +#[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/src/testing.rs b/contracts/core-token-vesting/tests/tests/test_vesting.rs similarity index 89% rename from contracts/core-token-vesting/src/testing.rs rename to contracts/core-token-vesting/tests/tests/test_vesting.rs index 02986a4..e8f986a 100644 --- a/contracts/core-token-vesting/src/testing.rs +++ b/contracts/core-token-vesting/tests/tests/test_vesting.rs @@ -1,20 +1,21 @@ -use crate::contract::tests::TestResult; -use crate::contract::{execute, instantiate, query}; -use crate::errors::{CliffError, ContractError, VestingError}; -use crate::msg::{ +use anyhow::anyhow; +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 { @@ -142,7 +143,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, @@ -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(); @@ -873,3 +927,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(()) +}