#![cfg(not(tarpaulin_include))] use cosmwasm_std::{coin, coins, to_json_binary, Addr, StdResult, Timestamp, Uint128}; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; use cw20_base::msg::InstantiateMsg as TokenInstantiateMsg; use cw_multi_test::{App, ContractWrapper, Executor}; use cw_utils::PaymentError; use astroport::asset::{native_asset_info, token_asset_info, AssetInfo}; use astroport::astro_converter; use astroport::astro_converter::OutpostBurnParams; use astroport::querier::query_balance; use astroport::vesting::{ Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, VestingAccount, VestingSchedule, VestingSchedulePoint, }; use astroport::vesting::{QueryMsg, VestingAccountResponse, VestingAccountsResponse, VestingInfo}; use astroport_vesting::error::ContractError; use astroport_vesting::state::Config; const OWNER1: &str = "owner1"; const USER1: &str = "user1"; const USER2: &str = "user2"; const TOKEN_INITIAL_AMOUNT: u128 = 1_000_000_000_000000; const IBC_ASTRO: &str = "ibc/ASTRO-TOKEN"; const NEW_ASTRO_DENOM: &str = "astro"; #[test] fn claim() { let user1 = Addr::unchecked(USER1); let owner = Addr::unchecked(OWNER1); let mut app = mock_app(&owner); let token_code_id = store_token_code(&mut app); let astro_token_instance = instantiate_token(&mut app, token_code_id, "ASTRO", Some(1_000_000_000_000000)); let vesting_instance = instantiate_vesting(&mut app, &astro_token_instance); let current_time = app.block_info().time.seconds(); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![ VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 101, amount: Uint128::new(200), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 110, amount: Uint128::new(100), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 200, amount: Uint128::new(100), }), }, ], }], }) .unwrap(), amount: Uint128::from(300u128), }; let res = app .execute_contract(owner.clone(), astro_token_instance.clone(), &msg, &[]) .unwrap_err(); assert_eq!( res.root_cause().to_string(), "Vesting schedule amount error. The total amount should be equal to the received amount." ); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![ VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 101, amount: Uint128::new(100), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 110, amount: Uint128::new(100), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 200, amount: Uint128::new(100), }), }, ], }], }) .unwrap(), amount: Uint128::from(300u128), }; app.execute_contract(owner.clone(), astro_token_instance.clone(), &msg, &[]) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(200); b.height += 200 / 5 }); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; let user1_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(user1_vesting_amount.clone(), Uint128::new(300u128)); // Check owner balance check_token_balance( &mut app, &astro_token_instance, &owner.clone(), TOKEN_INITIAL_AMOUNT - 300u128, ); // Check vesting balance check_token_balance( &mut app, &astro_token_instance, &vesting_instance.clone(), 300u128, ); let msg = ExecuteMsg::Claim { recipient: None, amount: None, }; let _res = app .execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) .unwrap(); let msg = QueryMsg::VestingAccount { address: user1.to_string(), }; let vesting_res: VestingAccountResponse = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(vesting_res.info.released_amount, Uint128::from(300u128)); // Check vesting balance check_token_balance( &mut app, &astro_token_instance, &vesting_instance.clone(), 0u128, ); // Check user balance check_token_balance(&mut app, &astro_token_instance, &user1.clone(), 300u128); // Owner balance mustn't change after claim check_token_balance( &mut app, &astro_token_instance, &owner.clone(), TOKEN_INITIAL_AMOUNT - 300u128, ); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; // Check user balance after claim let user1_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(user1_vesting_amount.clone(), Uint128::new(0u128)); } #[test] fn claim_native() { let user1 = Addr::unchecked(USER1); let owner = Addr::unchecked(OWNER1); let mut app = mock_app(&owner); let token_code_id = store_token_code(&mut app); let random_token_instance = instantiate_token(&mut app, token_code_id, "RND", Some(1_000_000000)); mint_tokens(&mut app, &random_token_instance, &owner, 1_000_000000); let vesting_instance = instantiate_vesting_remote_chain(&mut app); let current_time = app.block_info().time.seconds(); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 101, amount: Uint128::new(200), }), }], }], }) .unwrap(), amount: Uint128::from(300u128), }; let err = app .execute_contract(owner.clone(), random_token_instance.clone(), &msg, &[]) .unwrap_err(); assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); let msg = ExecuteMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![ VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 101, amount: Uint128::new(100), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 110, amount: Uint128::new(100), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 200, amount: Uint128::new(100), }), }, ], }], }; app.execute_contract( owner.clone(), vesting_instance.clone(), &msg, &coins(300, IBC_ASTRO), ) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(200); b.height += 200 / 5 }); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; let user1_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(user1_vesting_amount.clone(), Uint128::new(300u128)); // Check owner balance let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 300u128); // Check vesting balance let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, 300u128); let msg = ExecuteMsg::Claim { recipient: None, amount: None, }; app.execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) .unwrap(); let vesting_res: VestingAccountResponse = app .wrap() .query_wasm_smart( vesting_instance.clone(), &QueryMsg::VestingAccount { address: user1.to_string(), }, ) .unwrap(); assert_eq!(vesting_res.info.released_amount, Uint128::from(300u128)); // Check vesting balance let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, 0); // Check user balance let bal = query_balance(&app.wrap(), &user1, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, 300); // Owner balance mustn't change after claim let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 300u128); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; // Check user balance after claim let user1_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(user1_vesting_amount.clone(), Uint128::new(0u128)); } #[test] fn claim_after_migration() { let user1 = Addr::unchecked(USER1); let owner = Addr::unchecked(OWNER1); let mut app = mock_app(&owner); let current_time = app.block_info().time.seconds(); let vesting_instance = instantiate_vesting_131(&mut app); let msg = ExecuteMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 100_000, amount: Uint128::new(100_000), }), }], }], }; app.execute_contract( owner.clone(), vesting_instance.clone(), &msg, &coins(100_000, IBC_ASTRO), ) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(20_000); }); let claim_msg = ExecuteMsg::Claim { recipient: None, amount: None, }; app.execute_contract(user1.clone(), vesting_instance.clone(), &claim_msg, &[]) .unwrap(); let user_bal = query_balance(&app.wrap(), &user1, IBC_ASTRO).unwrap(); assert_eq!(user_bal.u128(), 20_000); // Init converter and migrate vesting migrate_vesting(&mut app, &vesting_instance); app.update_block(|b| { b.time = b.time.plus_seconds(20_000); }); app.execute_contract(user1.clone(), vesting_instance.clone(), &claim_msg, &[]) .unwrap(); // Old astro balance stays the same let old_astro_bal = query_balance(&app.wrap(), &user1, IBC_ASTRO).unwrap(); assert_eq!(old_astro_bal.u128(), 20_000); // Claimed new ASTRO let new_astro_bal = query_balance(&app.wrap(), &user1, NEW_ASTRO_DENOM).unwrap(); assert_eq!(new_astro_bal.u128(), 20_000); // Vesting converted all the old ASTRO to the new ASTRO let vesting_bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO).unwrap(); assert_eq!(vesting_bal.u128(), 0); let vesting_bal = query_balance(&app.wrap(), &vesting_instance, NEW_ASTRO_DENOM).unwrap(); assert_eq!(vesting_bal.u128(), 60_000); // Old arithmetic in vesting preserved let msg = QueryMsg::VestingAccount { address: user1.to_string(), }; let vesting_res: VestingAccountResponse = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(vesting_res.info.released_amount, Uint128::from(40_000u128)); // Pass full vesting period app.update_block(|b| { b.time = b.time.plus_seconds(1_000_000); }); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; let user1_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(user1_vesting_amount.clone(), Uint128::new(60_000u128)); // Assert new asset info in config let config: Config = app .wrap() .query_wasm_smart(vesting_instance.clone(), &QueryMsg::Config {}) .unwrap(); assert_eq!(config.vesting_token, AssetInfo::native(NEW_ASTRO_DENOM)); } #[test] fn register_vesting_accounts() { let user1 = Addr::unchecked(USER1); let user2 = Addr::unchecked(USER2); let owner = Addr::unchecked(OWNER1); let mut app = mock_app(&owner); let token_code_id = store_token_code(&mut app); let astro_token_instance = instantiate_token(&mut app, token_code_id, "ASTRO", Some(1_000_000_000_000000)); let noname_token_instance = instantiate_token( &mut app, token_code_id, "NONAME", Some(1_000_000_000_000000), ); mint_tokens( &mut app, &noname_token_instance, &owner, TOKEN_INITIAL_AMOUNT, ); let vesting_instance = instantiate_vesting(&mut app, &astro_token_instance); let current_time = app.block_info().time.seconds(); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 150, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 100, amount: Uint128::new(100), }), }], }], }) .unwrap(), amount: Uint128::from(100u128), }; let res = app .execute_contract(owner.clone(), astro_token_instance.clone(), &msg, &[]) .unwrap_err(); assert_eq!(res.root_cause().to_string(), "Vesting schedule error on addr: user1. Should satisfy: (start < end, end > current_time and start_amount < end_amount)"); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 150, amount: Uint128::new(100), }), }], }], }) .unwrap(), amount: Uint128::from(100u128), }; let res = app .execute_contract( user1.clone(), astro_token_instance.clone(), &msg.clone(), &[], ) .unwrap_err(); assert_eq!( res.root_cause().to_string(), "Overflow: Cannot Sub with 0 and 100" ); let res = app .execute_contract(owner.clone(), noname_token_instance.clone(), &msg, &[]) .unwrap_err(); assert_eq!(res.root_cause().to_string(), "Unauthorized"); // Checking that execute endpoint with native coin is unreachable if ASTRO is a cw20 token let native_msg = ExecuteMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 150, amount: Uint128::new(100), }), }], }], }; let err = app .execute_contract( owner.clone(), vesting_instance.clone(), &native_msg, &coins(100u128, "random-coin"), ) .unwrap_err(); assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); let _res = app .execute_contract(owner.clone(), astro_token_instance.clone(), &msg, &[]) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(150); b.height += 150 / 5 }); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; let user1_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(user1_vesting_amount.clone(), Uint128::new(100u128)); check_token_balance( &mut app, &astro_token_instance, &owner.clone(), TOKEN_INITIAL_AMOUNT - 100u128, ); check_token_balance( &mut app, &astro_token_instance, &vesting_instance.clone(), 100u128, ); let current_time = app.block_info().time.seconds(); // Let's check user1's final vesting amount after add schedule for a new one let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user2.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 150, amount: Uint128::new(200), }), }], }], }) .unwrap(), amount: Uint128::from(200u128), }; let _res = app .execute_contract(owner.clone(), astro_token_instance.clone(), &msg, &[]) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(150); b.height += 150 / 5 }); let msg = QueryMsg::AvailableAmount { address: user2.to_string(), }; let user2_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); check_token_balance( &mut app, &astro_token_instance, &owner.clone(), TOKEN_INITIAL_AMOUNT - 300u128, ); check_token_balance( &mut app, &astro_token_instance, &vesting_instance.clone(), 300u128, ); // A new schedule has been added successfully and an old one hasn't changed. // The new schedule doesn't have the same value as the old one. assert_eq!(user2_vesting_amount, Uint128::new(200u128)); assert_eq!(user1_vesting_amount, Uint128::from(100u128)); let current_time = app.block_info().time.seconds(); // Add one more vesting schedule; final amount to vest must increase let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 200, amount: Uint128::new(10), }), }], }], }) .unwrap(), amount: Uint128::from(10u128), }; let _res = app .execute_contract(owner.clone(), astro_token_instance.clone(), &msg, &[]) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(200); b.height += 200 / 5 }); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; let vesting_res: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(vesting_res, Uint128::new(110u128)); check_token_balance( &mut app, &astro_token_instance, &owner.clone(), TOKEN_INITIAL_AMOUNT - 310u128, ); check_token_balance( &mut app, &astro_token_instance, &vesting_instance.clone(), 310u128, ); let msg = ExecuteMsg::Claim { recipient: None, amount: None, }; let _res = app .execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) .unwrap(); let msg = QueryMsg::VestingAccount { address: user1.to_string(), }; let vesting_res: VestingAccountResponse = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(vesting_res.info.released_amount, Uint128::from(110u128)); check_token_balance( &mut app, &astro_token_instance, &vesting_instance.clone(), 200u128, ); check_token_balance(&mut app, &astro_token_instance, &user1.clone(), 110u128); // Owner balance mustn't change after claim check_token_balance( &mut app, &astro_token_instance, &owner.clone(), TOKEN_INITIAL_AMOUNT - 310u128, ); assert_eq!( app.wrap() .query_wasm_smart::( vesting_instance, &QueryMsg::VestingAccounts { start_after: None, limit: None, order_by: None } ) .unwrap(), VestingAccountsResponse { vesting_accounts: vec![ VestingAccountResponse { address: user2, info: VestingInfo { schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: 1571797669, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: 1571797719, amount: Uint128::new(200), }), }], released_amount: Uint128::zero(), } }, VestingAccountResponse { address: user1, info: VestingInfo { schedules: vec![ VestingSchedule { start_point: VestingSchedulePoint { time: 1571797819, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: 1571797919, amount: Uint128::new(10), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: 1571797519, amount: Uint128::new(0), }, end_point: Some(VestingSchedulePoint { time: 1571797569, amount: Uint128::new(100), }) } ], released_amount: Uint128::new(110), } } ] } ); } #[test] fn register_vesting_accounts_native() { let user1 = Addr::unchecked(USER1); let user2 = Addr::unchecked(USER2); let owner = Addr::unchecked(OWNER1); let mut app = mock_app(&owner); let token_code_id = store_token_code(&mut app); let random_token_instance = instantiate_token(&mut app, token_code_id, "RND", Some(1_000_000_000_000000)); mint_tokens( &mut app, &random_token_instance, &owner, TOKEN_INITIAL_AMOUNT, ); let vesting_instance = instantiate_vesting_remote_chain(&mut app); let current_time = app.block_info().time.seconds(); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 150, amount: Uint128::new(100), }), }], }], }) .unwrap(), amount: Uint128::from(100u128), }; let err = app .execute_contract(owner.clone(), random_token_instance.clone(), &msg, &[]) .unwrap_err(); assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); // Checking that execute endpoint with random native coin is unreachable let native_msg = ExecuteMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 150, amount: Uint128::new(100), }), }], }], }; let err = app .execute_contract( owner.clone(), vesting_instance.clone(), &native_msg, &coins(100u128, "random-coin"), ) .unwrap_err(); assert_eq!( ContractError::PaymentError(PaymentError::MissingDenom("ibc/ASTRO-TOKEN".to_string())), err.downcast().unwrap() ); app.execute_contract( owner.clone(), vesting_instance.clone(), &native_msg, &coins(100u128, IBC_ASTRO), ) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(150); b.height += 150 / 5 }); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; let user1_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(&vesting_instance, &msg) .unwrap(); assert_eq!(user1_vesting_amount.u128(), 100u128); let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 100u128); let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, 100); let current_time = app.block_info().time.seconds(); // Let's check user1's final vesting amount after add schedule for a new one let msg = ExecuteMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user2.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 150, amount: Uint128::new(200), }), }], }], }; app.execute_contract( owner.clone(), vesting_instance.clone(), &msg, &coins(200, IBC_ASTRO), ) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(150); b.height += 150 / 5 }); let msg = QueryMsg::AvailableAmount { address: user2.to_string(), }; let user2_vesting_amount: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 300u128); let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, 300u128); // A new schedule has been added successfully and an old one hasn't changed. // The new schedule doesn't have the same value as the old one. assert_eq!(user2_vesting_amount, Uint128::new(200u128)); assert_eq!(user1_vesting_amount, Uint128::from(100u128)); let current_time = app.block_info().time.seconds(); // Add one more vesting schedule; final amount to vest must increase let msg = ExecuteMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: current_time + 100, amount: Uint128::zero(), }, end_point: Some(VestingSchedulePoint { time: current_time + 200, amount: Uint128::new(10), }), }], }], }; app.execute_contract( owner.clone(), vesting_instance.clone(), &msg, &coins(10, IBC_ASTRO), ) .unwrap(); app.update_block(|b| { b.time = b.time.plus_seconds(200); b.height += 200 / 5 }); let msg = QueryMsg::AvailableAmount { address: user1.to_string(), }; let vesting_res: Uint128 = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(vesting_res, Uint128::new(110u128)); let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 310u128); let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, 310u128); let msg = ExecuteMsg::Claim { recipient: None, amount: None, }; let _res = app .execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) .unwrap(); let msg = QueryMsg::VestingAccount { address: user1.to_string(), }; let vesting_res: VestingAccountResponse = app .wrap() .query_wasm_smart(vesting_instance.clone(), &msg) .unwrap(); assert_eq!(vesting_res.info.released_amount, Uint128::from(110u128)); let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, 200); let bal = query_balance(&app.wrap(), &user1, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, 110u128); let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) .unwrap() .u128(); assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 310u128); } #[test] fn withdraw_from_active_schedule() { let owner = Addr::unchecked(OWNER1); let mut app = mock_app(&owner); let token_code_id = store_token_code(&mut app); let astro_token = instantiate_token(&mut app, token_code_id, "Astro", None); let vesting_instance = instantiate_vesting(&mut app, &astro_token); let user1 = Addr::unchecked("user1"); let vested_amount = Uint128::new(100_000_000_000000); let start_time = 1654599600; let end_time = 1686135600; let now_ts = 1675159485; app.update_block(|b| b.time = Timestamp::from_seconds(start_time)); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![VestingSchedule { start_point: VestingSchedulePoint { time: start_time, amount: Uint128::new(1_000_000_000000), }, end_point: Some(VestingSchedulePoint { time: end_time, amount: Uint128::new(100_000_000_000000), }), }], }], }) .unwrap(), amount: vested_amount, }; app.execute_contract(owner.clone(), astro_token.clone(), &msg, &[]) .unwrap(); app.update_block(|b| b.time = Timestamp::from_seconds(now_ts)); // Claim and check current amount claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, 65_543_017_979452, ); let withdraw_amount = Uint128::new(10_000_000_000000); let recipient = Addr::unchecked("recipient"); let withdraw_msg = ExecuteMsg::WithdrawFromActiveSchedule { account: user1.to_string(), recipient: Some(recipient.to_string()), withdraw_amount, }; app.execute_contract(owner.clone(), vesting_instance.clone(), &withdraw_msg, &[]) .unwrap(); // Recipient received tokens let recipient_bal = query_token_balance(&mut app, &astro_token, &recipient); assert_eq!(recipient_bal, withdraw_amount); // User1 did not receive tokens after withdraw event claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, 65_543_017_979452, ); app.update_block(|b| b.time = b.time.plus_seconds(86400 * 7)); // User1 available amount is still being increased but now with reduced slope claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, 66_890_633_481478, ); app.update_block(|b| b.time = Timestamp::from_seconds(end_time)); // In the end of the schedule user1 receives all tokens minus withdrawn amount claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, (vested_amount - withdraw_amount).u128(), ); } #[test] fn withdraw_overlapping_schedules() { let owner = Addr::unchecked(OWNER1); let mut app = mock_app(&owner); let token_code_id = store_token_code(&mut app); let astro_token = instantiate_token(&mut app, token_code_id, "Astro", None); let vesting_instance = instantiate_vesting(&mut app, &astro_token); let user1 = Addr::unchecked("user1"); let vested_amount = Uint128::new(100_000_000_000000); let start_time = 1654599600; let end_time = 1686135600; let now_ts = 1675159485; app.update_block(|b| b.time = Timestamp::from_seconds(start_time)); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![ VestingSchedule { start_point: VestingSchedulePoint { time: start_time, amount: Uint128::new(1_000_000_000000), }, end_point: Some(VestingSchedulePoint { time: end_time, amount: Uint128::new(50_000_000_000000), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: now_ts - 86400 * 7, amount: Uint128::new(50_000_000_000000), }, end_point: None, }, ], }], }) .unwrap(), amount: vested_amount, }; app.execute_contract(owner.clone(), astro_token.clone(), &msg, &[]) .unwrap(); app.update_block(|b| b.time = Timestamp::from_seconds(now_ts)); claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, 82_945_534_151445, ); let withdraw_amount = Uint128::new(10_000_000_000000); let recipient = Addr::unchecked("recipient"); let withdraw_msg = ExecuteMsg::WithdrawFromActiveSchedule { account: user1.to_string(), recipient: Some(recipient.to_string()), withdraw_amount, }; // Since we do not consider schedule without end point as active it is possible to withdraw from // active schedule with end_point. app.execute_contract(owner.clone(), vesting_instance.clone(), &withdraw_msg, &[]) .unwrap(); // Recipient received tokens let recipient_bal = query_token_balance(&mut app, &astro_token, &recipient); assert_eq!(recipient_bal, withdraw_amount); // User1 did not receive tokens after withdraw event claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, 82_945_534_151445, ); // Go to the end of the 1st schedule app.update_block(|b| b.time = Timestamp::from_seconds(end_time)); // In the end of the schedule user1 receives all tokens minus withdrawn amount claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, (vested_amount - withdraw_amount).u128(), ); } #[test] fn withdraw_overlapping_schedules2() { let owner = Addr::unchecked(OWNER1); let mut app = mock_app(&owner); let token_code_id = store_token_code(&mut app); let astro_token = instantiate_token(&mut app, token_code_id, "Astro", None); let vesting_instance = instantiate_vesting(&mut app, &astro_token); let user1 = Addr::unchecked("user1"); let vested_amount = Uint128::new(100_000_000_000000); let start_time = 1654599600; let end_time = 1686135600; let now_ts = 1675159485; app.update_block(|b| b.time = Timestamp::from_seconds(start_time)); let msg = Cw20ExecuteMsg::Send { contract: vesting_instance.to_string(), msg: to_json_binary(&Cw20HookMsg::RegisterVestingAccounts { vesting_accounts: vec![VestingAccount { address: user1.to_string(), schedules: vec![ VestingSchedule { start_point: VestingSchedulePoint { time: start_time, amount: Uint128::new(1_000_000_000000), }, end_point: Some(VestingSchedulePoint { time: end_time, amount: Uint128::new(50_000_000_000000), }), }, VestingSchedule { start_point: VestingSchedulePoint { time: now_ts - 86400 * 7, amount: Uint128::new(1_000_000_000000), }, end_point: Some(VestingSchedulePoint { time: end_time + 86400 * 7, amount: Uint128::new(50_000_000_000000), }), }, ], }], }) .unwrap(), amount: vested_amount, }; app.execute_contract(owner.clone(), astro_token.clone(), &msg, &[]) .unwrap(); app.update_block(|b| b.time = Timestamp::from_seconds(now_ts)); claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, 36_377_496_494237, ); let recipient = Addr::unchecked("recipient"); let withdraw_msg = ExecuteMsg::WithdrawFromActiveSchedule { account: user1.to_string(), recipient: Some(recipient.to_string()), withdraw_amount: Uint128::new(10_000_000_000000), }; let err = app .execute_contract(owner.clone(), vesting_instance.clone(), &withdraw_msg, &[]) .unwrap_err(); assert_eq!( ContractError::MultipleActiveSchedules(user1.to_string()), err.downcast().unwrap(), ); // Go to the end of the 1st schedule app.update_block(|b| b.time = Timestamp::from_seconds(end_time)); // Trying to withdraw again let err = app .execute_contract(owner.clone(), vesting_instance.clone(), &withdraw_msg, &[]) .unwrap_err(); // There is no 10M ASTRO available for withdrawal assert_eq!( ContractError::NotEnoughTokens(Uint128::new(2_431_962_342793)), err.downcast().unwrap(), ); claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, 97_568_037_657_207, ); // Withdrawing 1M ASTRO let withdraw_amount = Uint128::new(1_000_000_000000); let withdraw_msg = ExecuteMsg::WithdrawFromActiveSchedule { account: user1.to_string(), recipient: Some(recipient.to_string()), withdraw_amount, }; app.execute_contract(owner.clone(), vesting_instance.clone(), &withdraw_msg, &[]) .unwrap(); // Recipient received tokens let recipient_bal = query_token_balance(&mut app, &astro_token, &recipient); assert_eq!(recipient_bal, withdraw_amount); // user1's amount was not changed claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, 97_568_037_657_207, ); // Go to the end of the 2nd schedule app.update_block(|b| b.time = Timestamp::from_seconds(end_time + 86400 * 7)); // user1 received all tokens except 1M ASTRO claim_and_check( &mut app, &user1, &vesting_instance, &astro_token, (vested_amount - withdraw_amount).u128(), ); } fn mock_app(owner: &Addr) -> App { App::new(|app, _, storage| { app.bank .init_balance( storage, owner, vec![ coin(TOKEN_INITIAL_AMOUNT, IBC_ASTRO), coin(1_000_0000000u128, "random-coin"), ], ) .unwrap() }) } fn store_token_code(app: &mut App) -> u64 { let astro_token_contract = Box::new(ContractWrapper::new_with_empty( cw20_base::contract::execute, cw20_base::contract::instantiate, cw20_base::contract::query, )); app.store_code(astro_token_contract) } fn instantiate_token(app: &mut App, token_code_id: u64, name: &str, cap: Option) -> Addr { let name = String::from(name); let msg = TokenInstantiateMsg { name: name.clone(), symbol: name.clone(), decimals: 6, initial_balances: vec![], mint: Some(MinterResponse { minter: String::from(OWNER1), cap: cap.map(|v| Uint128::from(v)), }), marketing: None, }; app.instantiate_contract( token_code_id, Addr::unchecked(OWNER1), &msg, &[], name, None, ) .unwrap() } fn instantiate_vesting(mut app: &mut App, astro_token_instance: &Addr) -> Addr { let vesting_contract = Box::new(ContractWrapper::new_with_empty( astroport_vesting::contract::execute, astroport_vesting::contract::instantiate, astroport_vesting::contract::query, )); let owner = Addr::unchecked(OWNER1); let vesting_code_id = app.store_code(vesting_contract); let init_msg = InstantiateMsg { owner: OWNER1.to_string(), vesting_token: token_asset_info(astro_token_instance.clone()), }; let vesting_instance = app .instantiate_contract( vesting_code_id, owner.clone(), &init_msg, &[], "Vesting", None, ) .unwrap(); let res: Config = app .wrap() .query_wasm_smart(vesting_instance.clone(), &QueryMsg::Config {}) .unwrap(); assert_eq!( astro_token_instance.to_string(), res.vesting_token.to_string() ); mint_tokens( &mut app, &astro_token_instance, &owner, TOKEN_INITIAL_AMOUNT, ); check_token_balance( &mut app, &astro_token_instance, &owner, TOKEN_INITIAL_AMOUNT, ); vesting_instance } fn instantiate_vesting_remote_chain(app: &mut App) -> Addr { let vesting_contract = Box::new(ContractWrapper::new_with_empty( astroport_vesting::contract::execute, astroport_vesting::contract::instantiate, astroport_vesting::contract::query, )); let owner = Addr::unchecked(OWNER1); let vesting_code_id = app.store_code(vesting_contract); let init_msg = InstantiateMsg { owner: OWNER1.to_string(), vesting_token: native_asset_info(IBC_ASTRO.to_string()), }; app.instantiate_contract( vesting_code_id, owner.clone(), &init_msg, &[], "Vesting", None, ) .unwrap() } fn instantiate_vesting_131(app: &mut App) -> Addr { let vesting_contract = Box::new(ContractWrapper::new_with_empty( astroport_vesting_131::contract::execute, astroport_vesting_131::contract::instantiate, astroport_vesting_131::contract::query, )); let owner = Addr::unchecked(OWNER1); let vesting_code_id = app.store_code(vesting_contract); let init_msg = InstantiateMsg { owner: OWNER1.to_string(), vesting_token: native_asset_info(IBC_ASTRO.to_string()), }; app.instantiate_contract( vesting_code_id, owner.clone(), &init_msg, &[], "Vesting", Some(OWNER1.to_string()), ) .unwrap() } fn migrate_vesting(app: &mut App, vesting: &Addr) { // Setup converter let converter_contract = Box::new(ContractWrapper::new_with_empty( astro_token_converter::contract::execute, astro_token_converter::contract::instantiate, astro_token_converter::contract::query, )); let converter_code_id = app.store_code(converter_contract); let msg = astro_converter::InstantiateMsg { old_astro_asset_info: AssetInfo::native(IBC_ASTRO), new_astro_denom: NEW_ASTRO_DENOM.to_string(), outpost_burn_params: Some(OutpostBurnParams { terra_burn_addr: "terra1xxxx".to_string(), old_astro_transfer_channel: "channel-228".to_string(), }), }; let converter_contract = app .instantiate_contract( converter_code_id, Addr::unchecked(OWNER1), &msg, &[], "Converter", None, ) .unwrap(); app.init_modules(|app, _, storage| { app.bank .init_balance( storage, &converter_contract, vec![coin(u128::MAX, NEW_ASTRO_DENOM)], ) .unwrap() }); let vesting_contract = Box::new( ContractWrapper::new_with_empty( astroport_vesting::contract::execute, astroport_vesting::contract::instantiate, astroport_vesting::contract::query, ) .with_migrate(astroport_vesting::contract::migrate), ); let vesting_code_id = app.store_code(vesting_contract); app.migrate_contract( Addr::unchecked(OWNER1), vesting.clone(), &MigrateMsg { converter_contract: converter_contract.to_string(), }, vesting_code_id, ) .unwrap(); } fn mint_tokens(app: &mut App, token: &Addr, recipient: &Addr, amount: u128) { let msg = Cw20ExecuteMsg::Mint { recipient: recipient.to_string(), amount: Uint128::from(amount), }; app.execute_contract(Addr::unchecked(OWNER1), token.to_owned(), &msg, &[]) .unwrap(); } fn check_token_balance(app: &mut App, token: &Addr, address: &Addr, expected: u128) { let msg = Cw20QueryMsg::Balance { address: address.to_string(), }; let res: StdResult = app.wrap().query_wasm_smart(token, &msg); assert_eq!(res.unwrap().balance, Uint128::from(expected)); } fn query_token_balance(app: &mut App, token: &Addr, address: &Addr) -> Uint128 { let msg = Cw20QueryMsg::Balance { address: address.to_string(), }; let res: BalanceResponse = app.wrap().query_wasm_smart(token, &msg).unwrap(); res.balance } fn claim_and_check( app: &mut App, who: &Addr, vesting: &Addr, astro_token: &Addr, expected_amount: u128, ) { app.execute_contract( who.clone(), vesting.clone(), &ExecuteMsg::Claim { recipient: None, amount: None, }, &[], ) .unwrap(); let astro_amount = query_token_balance(app, &astro_token, &who); assert_eq!(astro_amount.u128(), expected_amount); }