diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index eabe9527..a59f8967 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -6,6 +6,12 @@ repository = { workspace = true } edition = { workspace = true } license = { workspace = true } +[features] +# Enables minter feature on the vesting contract +# if enabled, a specified address can mint/burn tokens +minter = [] +default = [] + [lib] crate-type = ["cdylib"] diff --git a/contracts/vesting/Makefile b/contracts/vesting/Makefile index 918bdcb5..573bac7e 100644 --- a/contracts/vesting/Makefile +++ b/contracts/vesting/Makefile @@ -3,7 +3,7 @@ default: all all: lint build test test: build - cargo test + cargo test --all-features build: $(MAKE) -C ../token build || break; @@ -15,7 +15,7 @@ fmt: cargo fmt --all clippy: build - cargo clippy --all-targets -- -D warnings + cargo clippy --all-targets --all-features -- -D warnings clean: cargo clean diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 51945cc9..dea61aad 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -2,19 +2,17 @@ use soroban_sdk::{ contract, contractimpl, contractmeta, log, panic_with_error, Address, BytesN, Env, Vec, }; -use curve::Curve; - -use crate::storage::{ - get_admin, get_token_info, save_max_vesting_complexity, save_token_info, DistributionInfo, -}; -use crate::utils::{create_vesting_accounts, verify_vesting_and_update_balances}; +#[cfg(feature = "minter")] +use crate::storage::{get_minter, save_minter, MinterInfo}; use crate::{ error::ContractError, storage::{ - get_minter, get_vesting, save_admin, save_minter, MinterInfo, VestingBalance, - VestingTokenInfo, + get_admin, get_all_vestings, get_max_vesting_complexity, get_token_info, get_vesting, + save_admin, save_max_vesting_complexity, save_token_info, save_vesting, update_vesting, + VestingInfo, VestingSchedule, VestingTokenInfo, }, token_contract, + utils::{check_duplications, validate_vesting_schedule}, }; // Metadata that is added on to the WASM custom section @@ -30,62 +28,50 @@ pub trait VestingTrait { env: Env, admin: Address, vesting_token: VestingTokenInfo, - vesting_balances: Vec, - minter_info: Option, max_vesting_complexity: u32, ); - fn transfer_token(env: Env, sender: Address, recipient: Address, amount: i128); + fn create_vesting_schedules(env: Env, vesting_accounts: Vec); - fn claim(env: Env, sender: Address); + fn claim(env: Env, sender: Address, index: u64); - fn burn(env: Env, sender: Address, amount: u128); + fn update(env: Env, new_wash_hash: BytesN<32>); - fn mint(env: Env, sender: Address, amount: i128); + fn query_balance(env: Env, address: Address) -> i128; - // TODO: we will need these in the future, not needed for the most basic implementation right now - // TODO: replace the tuple `owner_spender: (Address, Address)` with how it is in `send_to_contract_from` - // fn increase_allowance(env: Env, owner_spender: (Address, Address), amount: i128); - - // fn decrease_allowance(env: Env, owner_spender: (Address, Address), amount: i128); - - // fn transfer_from( - // env: Env, - // owner_spender: (Address, Address), - // to: Address, - // amount: i128, - // ) -> Result<(), ContractError>; - - // fn burn_from( - // env: Env, - // sender: Address, - // owner: Address, - // amount: i128, - // ) -> Result<(), ContractError>; - - // fn send_to_contract_from( - // env: Env, - // sender: Address, - // owner: Address, - // contract: Address, - // amount: i128, - // ) -> Result<(), ContractError>; + fn query_vesting_info(env: Env, address: Address, index: u64) -> VestingInfo; - fn update_minter(env: Env, sender: Address, new_minter: Address); + fn query_all_vesting_info(env: Env, address: Address) -> Vec; - fn update_minter_capacity(env: Env, sender: Address, new_capacity: u128); + fn query_token_info(env: Env) -> VestingTokenInfo; - fn query_balance(env: Env, address: Address) -> i128; + fn query_vesting_contract_balance(env: Env) -> i128; - fn query_distribution_info(env: Env, address: Address) -> DistributionInfo; + fn query_available_to_claim(env: Env, address: Address, index: u64) -> i128; - fn query_token_info(env: Env) -> VestingTokenInfo; + #[cfg(feature = "minter")] + fn initialize_with_minter( + env: Env, + admin: Address, + vesting_token: VestingTokenInfo, + max_vesting_complexity: u32, + minter_info: MinterInfo, + ); - fn query_minter(env: Env) -> MinterInfo; + #[cfg(feature = "minter")] + fn burn(env: Env, sender: Address, amount: u128); - fn query_vesting_contract_balance(env: Env) -> i128; + #[cfg(feature = "minter")] + fn mint(env: Env, sender: Address, amount: i128); + + #[cfg(feature = "minter")] + fn update_minter(env: Env, sender: Address, new_minter: Address); + + #[cfg(feature = "minter")] + fn update_minter_capacity(env: Env, sender: Address, new_capacity: u128); - fn query_available_to_claim(env: Env, address: Address) -> i128; + #[cfg(feature = "minter")] + fn query_minter(env: Env) -> MinterInfo; } #[contractimpl] @@ -94,56 +80,35 @@ impl VestingTrait for Vesting { env: Env, admin: Address, vesting_token: VestingTokenInfo, - vesting_balances: Vec, - minter_info: Option, max_vesting_complexity: u32, ) { - admin.require_auth(); - save_admin(&env, &admin); - if vesting_balances.is_empty() { - log!( - &env, - "Vesting: Initialize: At least one vesting schedule must be provided." - ); - panic_with_error!(env, ContractError::MissingBalance); - } - - let total_vested_amount = - create_vesting_accounts(&env, max_vesting_complexity, vesting_balances); - - // check if the admin has enough tokens to start the vesting contract - let token_client = token_contract::Client::new(&env, &vesting_token.address); - - if token_client.balance(&admin) < total_vested_amount as i128 { - log!( - &env, - "Vesting: Initialize: Admin does not have enough tokens to start the vesting contract" - ); - panic_with_error!(env, ContractError::NoEnoughtTokensToStart); - } + let token_info = VestingTokenInfo { + name: vesting_token.name, + symbol: vesting_token.symbol, + decimals: vesting_token.decimals, + address: vesting_token.address, + }; - token_client.transfer( - &admin, - &env.current_contract_address(), - &(total_vested_amount as i128), - ); + save_token_info(&env, &token_info); + save_max_vesting_complexity(&env, &max_vesting_complexity); - if let Some(minter) = minter_info { - let input_curve = Curve::Constant(minter.mint_capacity); + env.events() + .publish(("Initialize", "Vesting contract with admin: "), admin); + } - let capacity = input_curve.value(env.ledger().timestamp()); + #[cfg(feature = "minter")] + fn initialize_with_minter( + env: Env, + admin: Address, + vesting_token: VestingTokenInfo, + max_vesting_complexity: u32, + minter_info: MinterInfo, + ) { + save_admin(&env, &admin); - if total_vested_amount > capacity { - log!( - &env, - "Vesting: Initialize: total vested amount over the capacity" - ); - panic_with_error!(env, ContractError::TotalVestedOverCapacity); - } - save_minter(&env, &minter); - } + save_minter(&env, &minter_info); let token_info = VestingTokenInfo { name: vesting_token.name, @@ -159,32 +124,72 @@ impl VestingTrait for Vesting { .publish(("Initialize", "Vesting contract with admin: "), admin); } - fn transfer_token(env: Env, sender: Address, recipient: Address, amount: i128) { - sender.require_auth(); + fn create_vesting_schedules(env: Env, vesting_schedules: Vec) { + let admin = get_admin(&env); + admin.require_auth(); - if amount <= 0 { - log!(&env, "Vesting: Transfer token: Invalid transfer amount"); - panic_with_error!(env, ContractError::InvalidTransferAmount); + if vesting_schedules.is_empty() { + log!( + &env, + "Vesting: Create vesting account: At least one vesting schedule must be provided." + ); + panic_with_error!(env, ContractError::MissingBalance); } - let token_client = token_contract::Client::new(&env, &get_token_info(&env).address); + check_duplications(&env, vesting_schedules.clone()); + let max_vesting_complexity = get_max_vesting_complexity(&env); - verify_vesting_and_update_balances(&env, &sender, amount as u128); - token_client.transfer(&env.current_contract_address(), &recipient, &amount); + let mut total_vested_amount = 0; - env.events().publish( - ( - "Transfer token", - "Transfering tokens between accounts: from: {}, to:{}, amount: {}", - ), - (sender, recipient, amount), + vesting_schedules.into_iter().for_each(|vesting_schedule| { + let vested_amount = validate_vesting_schedule(&env, &vesting_schedule.curve) + .expect("Invalid curve and amount"); + + if max_vesting_complexity <= vesting_schedule.curve.size() { + log!( + &env, + "Vesting: Create vesting account: Invalid curve complexity for {}", + vesting_schedule.recipient + ); + panic_with_error!(env, ContractError::VestingComplexityTooHigh); + } + + save_vesting( + &env, + &vesting_schedule.recipient.clone(), + &VestingInfo { + balance: vested_amount, + recipient: vesting_schedule.recipient, + schedule: vesting_schedule.curve.clone(), + }, + ); + + total_vested_amount += vested_amount; + }); + + // check if the admin has enough tokens to start the vesting contract + let vesting_token = get_token_info(&env); + let token_client = token_contract::Client::new(&env, &vesting_token.address); + + if token_client.balance(&admin) < total_vested_amount as i128 { + log!( + &env, + "Vesting: Create vesting account: Admin does not have enough tokens to start the vesting schedule" + ); + panic_with_error!(env, ContractError::NoEnoughtTokensToStart); + } + + token_client.transfer( + &admin, + &env.current_contract_address(), + &(total_vested_amount as i128), ); } - fn claim(env: Env, sender: Address) { + fn claim(env: Env, sender: Address, index: u64) { sender.require_auth(); - let available_to_claim = Self::query_available_to_claim(env.clone(), sender.clone()); + let available_to_claim = Self::query_available_to_claim(env.clone(), sender.clone(), index); if available_to_claim <= 0 { log!(&env, "Vesting: Claim: No tokens available to claim"); @@ -193,7 +198,31 @@ impl VestingTrait for Vesting { let token_client = token_contract::Client::new(&env, &get_token_info(&env).address); - verify_vesting_and_update_balances(&env, &sender, available_to_claim as u128); + let vesting_info = get_vesting(&env, &sender, index); + let vested = vesting_info.schedule.value(env.ledger().timestamp()); + + let sender_balance = vesting_info.balance; + let sender_liquid = sender_balance // this checks if we can withdraw any vesting + .checked_sub(vested) + .unwrap_or_else(|| panic_with_error!(env, ContractError::NotEnoughBalance)); + + if sender_liquid < available_to_claim as u128 { + log!( + &env, + "Vesting: Verify Vesting Update Balances: Remaining amount must be at least equal to vested amount" + ); + panic_with_error!(env, ContractError::CantMoveVestingTokens); + } + + update_vesting( + &env, + &sender, + index, + &VestingInfo { + balance: (sender_balance - available_to_claim as u128), + ..vesting_info + }, + ); token_client.transfer( &env.current_contract_address(), @@ -205,6 +234,7 @@ impl VestingTrait for Vesting { .publish(("Claim", "Claimed tokens: "), available_to_claim); } + #[cfg(feature = "minter")] fn burn(env: Env, sender: Address, amount: u128) { sender.require_auth(); @@ -221,6 +251,7 @@ impl VestingTrait for Vesting { env.events().publish(("Burn", "Burned tokens: "), amount); } + #[cfg(feature = "minter")] fn mint(env: Env, sender: Address, amount: i128) { sender.require_auth(); @@ -272,188 +303,7 @@ impl VestingTrait for Vesting { env.events().publish(("Mint", "Minted tokens: "), amount); } - // TODO: we will need these in the future, not needed for the most basic implementation right now - // fn increase_allowance(env: Env, owner_spender: (Address, Address), amount: i128) { - // owner_spender.0.require_auth(); - - // if amount <= 0 { - // log!(&env, "Vesting: Increase allowance: Invalid amount"); - // panic_with_error!(env, ContractError::InvalidAllowanceAmount); - // } - - // let allowance = get_allowances(&env, &owner_spender) - // .checked_add(amount) - // .unwrap_or_else(|| { - // log!( - // &env, - // "Vesting: Increase allowance: Critical error - allowance cannot be negative" - // ); - // panic_with_error!(env, ContractError::Std); - // }); - - // save_allowances(&env, &owner_spender, allowance); - - // env.events().publish( - // ( - // "Increase allowance", - // "Increased allowance between accounts: from: {}, to: {}, increase: {}", - // ), - // (owner_spender.0, owner_spender.1, amount), - // ); - // } - - // fn decrease_allowance(env: Env, owner_spender: (Address, Address), amount: i128) { - // owner_spender.0.require_auth(); - - // if amount <= 0 { - // log!(&env, "Vesting: Decrease allowance: Invalid amount"); - // panic_with_error!(env, ContractError::InvalidAllowanceAmount); - // } - - // let allowance = get_allowances(&env, &owner_spender) - // .checked_sub(amount) - // .unwrap_or_else(|| { - // log!( - // &env, - // "Vesting: Decrease allowance: Critical error - allowance cannot be negative" - // ); - // panic_with_error!(env, ContractError::Std); - // }); - - // save_allowances(&env, &owner_spender, allowance); - - // env.events().publish( - // ( - // "Decrease allowance", - // "Decreased allowance between accounts: from: {}, to: {}, decrease: {}", - // ), - // (owner_spender.0, owner_spender.1, amount), - // ); - // } - - // fn transfer_from( - // env: Env, - // owner_spender: (Address, Address), - // to: Address, - // amount: i128, - // ) -> Result<(), ContractError> { - // let owner = owner_spender.0.clone(); - // let spender = owner_spender.1.clone(); - // spender.require_auth(); - - // if amount <= 0 { - // log!(&env, "Vesting: Transfer from: Invalid transfer amount"); - // panic_with_error!(env, ContractError::InvalidTransferAmount); - // } - - // // todo deduct_allowances - // let allowance = get_allowances(&env, &owner_spender); - // if allowance < amount { - // log!(&env, "Vesting: Transfer from: Not enough allowance"); - // panic_with_error!(env, ContractError::NotEnoughBalance); - // } - // let new_allowance = allowance.checked_sub(amount).unwrap_or_else(|| { - // log!( - // &env, - // "Vesting: Transfer from: Critical error - allowance cannot be negative" - // ); - // panic_with_error!(env, ContractError::Std); - // }); - - // verify_vesting_and_transfer_tokens(&env, &owner, &to, amount)?; - - // save_allowances(&env, &owner_spender, new_allowance); - - // env.events().publish( - // ( - // "Transfer from", - // "Transfering tokens between accounts: from: {}, to: {}, amount: {}", - // ), - // (owner, to, amount), - // ); - - // Ok(()) - // } - - // fn burn_from( - // env: Env, - // sender: Address, - // owner: Address, - // amount: i128, - // ) -> Result<(), ContractError> { - // sender.require_auth(); - - // if amount <= 0 { - // log!(&env, "Vesting: Burn from: Invalid burn amount"); - // panic_with_error!(env, ContractError::InvalidBurnAmount); - // } - - // let allowance = get_allowances(&env, &(owner.clone(), sender.clone())); - // if allowance < amount { - // log!(&env, "Vesting: Burn from: Not enough allowance"); - // panic_with_error!(env, ContractError::NotEnoughBalance); - // } - - // let new_allowance = allowance.checked_sub(amount).unwrap_or_else(|| { - // log!( - // &env, - // "Vesting: Burn from: Critical error - allowance cannot be negative" - // ); - // panic_with_error!(env, ContractError::Std); - // }); - - // let total_supply = get_vesting_total_supply(&env) - // .checked_sub(amount) - // .unwrap_or_else(|| { - // log!( - // &env, - // "Vesting: Burn from: Critical error - total supply cannot be negative" - // ); - // panic_with_error!(env, ContractError::Std); - // }); - - // update_vesting_total_supply(&env, total_supply); - - // let token_client = token_contract::Client::new(&env, &get_config(&env).token_info.address); - // token_client.burn(&owner, &amount); - - // save_allowances(&env, &(owner, sender), new_allowance); - - // env.events() - // .publish(("Burn from", "Burned tokens: "), amount); - - // Ok(()) - // } - - // fn send_to_contract_from( - // env: Env, - // sender: Address, - // owner: Address, - // contract: Address, - // amount: i128, - // ) -> Result<(), ContractError> { - // sender.require_auth(); - // if amount <= 0 { - // log!(&env, "Vesting: Send to contract from: Invalid amount"); - // panic_with_error!(env, ContractError::InvalidTransferAmount); - // } - // //used to verify that the sender is authorized by the owner - // let _ = get_allowances(&env, &(owner.clone(), sender.clone())); - - // let token_client = token_contract::Client::new(&env, &get_config(&env).token_info.address); - // token_client.transfer(&owner, &contract, &amount); - - // env.events().publish( - // ( - // "Send to contract from", - // "Sent tokens to contract from account: from: {}, to: {}, amount: {}", - // ), - // (owner, contract, amount), - // ); - - // Ok(()) - // } - + #[cfg(feature = "minter")] fn update_minter(env: Env, sender: Address, new_minter: Address) { let current_minter = get_minter(&env); @@ -484,6 +334,7 @@ impl VestingTrait for Vesting { .publish(("Update minter", "Updated minter to: "), new_minter); } + #[cfg(feature = "minter")] fn update_minter_capacity(env: Env, sender: Address, new_capacity: u128) { if sender != get_admin(&env) { log!( @@ -516,14 +367,19 @@ impl VestingTrait for Vesting { token_contract::Client::new(&env, &get_token_info(&env).address).balance(&address) } - fn query_distribution_info(env: Env, address: Address) -> DistributionInfo { - get_vesting(&env, &address).distribution_info + fn query_vesting_info(env: Env, address: Address, index: u64) -> VestingInfo { + get_vesting(&env, &address, index) + } + + fn query_all_vesting_info(env: Env, address: Address) -> Vec { + get_all_vestings(&env, &address) } fn query_token_info(env: Env) -> VestingTokenInfo { get_token_info(&env) } + #[cfg(feature = "minter")] fn query_minter(env: Env) -> MinterInfo { if let Some(minter) = get_minter(&env) { minter @@ -538,26 +394,13 @@ impl VestingTrait for Vesting { token_contract::Client::new(&env, &token_address).balance(&env.current_contract_address()) } - fn query_available_to_claim(env: Env, address: Address) -> i128 { - let vesting_info = get_vesting(&env, &address); - let vested = vesting_info - .distribution_info - .get_curve() - .value(env.ledger().timestamp()); - - let sender_balance = vesting_info.balance; - let sender_liquid = sender_balance - .checked_sub(vested) - .unwrap_or_else(|| panic_with_error!(env, ContractError::NotEnoughBalance)); + fn query_available_to_claim(env: Env, address: Address, index: u64) -> i128 { + let vesting_info = get_vesting(&env, &address, index); - sender_liquid as i128 + (vesting_info.balance - vesting_info.schedule.value(env.ledger().timestamp())) as i128 } -} -#[contractimpl] -impl Vesting { - #[allow(dead_code)] - pub fn update(env: Env, new_wasm_hash: BytesN<32>) { + fn update(env: Env, new_wasm_hash: BytesN<32>) { let admin = get_admin(&env); admin.require_auth(); diff --git a/contracts/vesting/src/error.rs b/contracts/vesting/src/error.rs index f3a1654e..d7161123 100644 --- a/contracts/vesting/src/error.rs +++ b/contracts/vesting/src/error.rs @@ -32,6 +32,12 @@ pub enum ContractError { NoAddressesToAdd = 24, NoEnoughtTokensToStart = 25, NotEnoughBalance = 26, + + VestingBothPresent = 27, + VestingNonePresent = 28, + + CurveConstant = 29, + CurveSLNotDecreasing = 30, } impl From for ContractError { diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index 7ea507ce..af6e64c7 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,6 +1,7 @@ -use curve::{Curve, SaturatingLinear}; +use curve::Curve; use soroban_sdk::{ - contracttype, log, panic_with_error, Address, ConversionError, Env, String, TryFromVal, Val, + contracttype, log, panic_with_error, vec, Address, ConversionError, Env, String, TryFromVal, + Val, Vec, }; use crate::error::ContractError; @@ -33,52 +34,39 @@ pub struct VestingTokenInfo { pub address: Address, } +// This structure is used as an argument during the vesting account creation #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub struct VestingBalance { - pub rcpt_address: Address, - pub distribution_info: DistributionInfo, +pub struct VestingSchedule { + pub recipient: Address, + pub curve: Curve, } #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub struct MinterInfo { - pub address: Address, - pub mint_capacity: u128, -} - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DistributionInfo { - pub start_timestamp: u64, - pub end_timestamp: u64, - pub amount: u128, // this is fine. this will be constant for historical data checking +pub struct VestingInfo { + // the total amount of tokens left to be distributed + // it's updated during each claim + pub balance: u128, + pub recipient: Address, + pub schedule: Curve, } +#[cfg(feature = "minter")] #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub struct VestingInfo { - pub balance: u128, // This is the value that we will update during transfer msgs - pub distribution_info: DistributionInfo, +pub struct MinterInfo { + pub address: Address, + pub mint_capacity: u128, } +#[cfg(feature = "minter")] impl MinterInfo { pub fn get_curve(&self) -> Curve { Curve::Constant(self.mint_capacity) } } -impl DistributionInfo { - pub fn get_curve(&self) -> Curve { - Curve::SaturatingLinear(SaturatingLinear { - min_x: self.start_timestamp, - min_y: self.amount, - max_x: self.end_timestamp, - max_y: 0u128, - }) - } -} - pub fn save_admin(env: &Env, admin: &Address) { env.storage().persistent().set(&DataKey::Admin, admin); } @@ -93,46 +81,91 @@ pub fn get_admin(env: &Env) -> Address { }) } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VestingInfoKey { + pub recipient: Address, + pub index: u64, +} + pub fn save_vesting(env: &Env, address: &Address, vesting_info: &VestingInfo) { - env.storage().persistent().set(address, vesting_info); + let mut index = 0u64; + let mut vesting_key = VestingInfoKey { + recipient: address.clone(), + index, + }; + + // Find the next available index + while env.storage().persistent().has(&vesting_key) { + index += 1; + vesting_key = VestingInfoKey { + recipient: address.clone(), + index, + }; + } + + env.storage().persistent().set(&vesting_key, vesting_info); } -pub fn get_vesting(env: &Env, address: &Address) -> VestingInfo { - env.storage().persistent().get(address).unwrap_or_else(|| { +pub fn update_vesting(env: &Env, address: &Address, index: u64, vesting_info: &VestingInfo) { + let vesting_key = VestingInfoKey { + recipient: address.clone(), + index, + }; + env.storage().persistent().set(&vesting_key, vesting_info); +} + +pub fn get_vesting(env: &Env, recipient: &Address, index: u64) -> VestingInfo { + let vesting_key = VestingInfoKey { + recipient: recipient.clone(), + index, + }; + env.storage().persistent().get(&vesting_key).unwrap_or_else(|| { log!(&env, "Vesting: Get vesting schedule: Critical error - No vesting schedule found for the given address"); panic_with_error!(env, ContractError::VestingNotFoundForAddress); }) } -// TODO: uncomment when needed -// pub fn get_allowances(env: &Env, owner_spender: &(Address, Address)) -> i128 { -// env.storage().persistent().get(owner_spender).unwrap_or_else(|| { -// log!(&env, "Vesting: Get allowance: Critical error - No allowance found for the given address pair"); -// panic_with_error!(env, ContractError::AllowanceNotFoundForGivenPair); -// }) -// } +pub fn get_all_vestings(env: &Env, address: &Address) -> Vec { + let mut vestings = vec![&env]; + let mut index = 0u64; -// pub fn save_allowances(env: &Env, owner_spender: &(Address, Address), amount: i128) { -// env.storage().persistent().set(owner_spender, &amount); -// } + loop { + let vesting_key = VestingInfoKey { + recipient: address.clone(), + index, + }; + + if let Some(vesting_info) = env.storage().persistent().get(&vesting_key) { + vestings.push_back(vesting_info); + index += 1; + } else { + break; + } + } + vestings +} + +#[cfg(feature = "minter")] pub fn save_minter(env: &Env, minter: &MinterInfo) { - env.storage().persistent().set(&DataKey::Minter, minter); + env.storage().instance().set(&DataKey::Minter, minter); } +#[cfg(feature = "minter")] pub fn get_minter(env: &Env) -> Option { - env.storage().persistent().get(&DataKey::Minter) + env.storage().instance().get(&DataKey::Minter) } pub fn save_token_info(env: &Env, token_info: &VestingTokenInfo) { env.storage() - .persistent() + .instance() .set(&DataKey::VestingTokenInfo, token_info); } pub fn get_token_info(env: &Env) -> VestingTokenInfo { env.storage() - .persistent() + .instance() .get(&DataKey::VestingTokenInfo) .unwrap_or_else(|| { log!( @@ -145,6 +178,13 @@ pub fn get_token_info(env: &Env) -> VestingTokenInfo { pub fn save_max_vesting_complexity(env: &Env, max_vesting_complexity: &u32) { env.storage() - .persistent() + .instance() .set(&DataKey::MaxVestingComplexity, max_vesting_complexity); } + +pub fn get_max_vesting_complexity(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::MaxVestingComplexity) + .unwrap() +} diff --git a/contracts/vesting/src/tests.rs b/contracts/vesting/src/tests.rs index d5edbbdc..e19df2c7 100644 --- a/contracts/vesting/src/tests.rs +++ b/contracts/vesting/src/tests.rs @@ -1,4 +1,5 @@ +mod claim; mod instantiate; -mod messages; -pub mod setup; -mod transfer; +#[cfg(feature = "minter")] +mod minter; +mod setup; diff --git a/contracts/vesting/src/tests/transfer.rs b/contracts/vesting/src/tests/claim.rs similarity index 59% rename from contracts/vesting/src/tests/transfer.rs rename to contracts/vesting/src/tests/claim.rs index 640fda9f..a7131ea0 100644 --- a/contracts/vesting/src/tests/transfer.rs +++ b/contracts/vesting/src/tests/claim.rs @@ -1,7 +1,9 @@ use crate::{ - storage::{DistributionInfo, VestingBalance, VestingTokenInfo}, + storage::{VestingInfo, VestingSchedule, VestingTokenInfo}, tests::setup::instantiate_vesting_client, }; +use curve::{Curve, PiecewiseLinear, SaturatingLinear, Step}; + use soroban_sdk::{ testutils::{Address as _, Ledger}, vec, Address, Env, String, @@ -10,7 +12,7 @@ use soroban_sdk::{ use super::setup::deploy_token_contract; #[test] -fn transfer_tokens_when_fully_vested() { +fn claim_tokens_when_fully_vested() { let env = Env::default(); env.mock_all_auths(); env.budget().reset_unlimited(); @@ -28,23 +30,25 @@ fn transfer_tokens_when_fully_vested() { address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 0, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 120, + max_x: 60, + max_y: 0, + }), }, - VestingBalance { - rcpt_address: Address::generate(&env), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 200, - }, + VestingSchedule { + recipient: Address::generate(&env), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 15, + min_y: 200, + max_x: 60, + max_y: 0, + }), }, ]; @@ -53,7 +57,8 @@ fn transfer_tokens_when_fully_vested() { // admin has 320 vesting tokens prior to initializing the contract assert_eq!(token_client.balance(&admin), 320); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); // after initialization the admin has 0 vesting tokens // contract has 320 vesting tokens @@ -67,7 +72,7 @@ fn transfer_tokens_when_fully_vested() { env.ledger().with_mut(|li| li.timestamp = 60); // user collects the vested tokens and transfers them to himself - vesting_client.transfer_token(&vester1, &vester1, &120); + vesting_client.claim(&vester1, &0); // vester1 has 120 tokens after claiming the vested amount assert_eq!(vesting_client.query_balance(&vester1), 120); @@ -95,15 +100,16 @@ fn transfer_tokens_when_half_vested() { address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 0, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 120, + max_x: 60, + max_y: 0, + }), }, ]; @@ -112,7 +118,8 @@ fn transfer_tokens_when_half_vested() { // admin has 120 vesting tokens prior to initializing the contract assert_eq!(token_client.balance(&admin), 120); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); // after initialization the admin has 0 vesting tokens // contract has 120 vesting tokens @@ -125,12 +132,8 @@ fn transfer_tokens_when_half_vested() { // we move time to the middle of the vesting period env.ledger().with_mut(|li| li.timestamp = 30); - // user is greedy and tries to transfer more than he can - let result = vesting_client.try_transfer_token(&vester1, &vester1, &61); - assert!(result.is_err()); - // user collects the vested tokens and transfers them to himself - vesting_client.transfer_token(&vester1, &vester1, &60); + vesting_client.claim(&vester1, &0); // vester1 has 60 tokens after claiming the vested amount assert_eq!(vesting_client.query_balance(&vester1), 60); @@ -140,7 +143,7 @@ fn transfer_tokens_when_half_vested() { } #[test] -fn test_claim_tokens_once_then_claim_again() { +fn claim_tokens_once_then_claim_again() { let env = Env::default(); env.mock_all_auths(); env.budget().reset_unlimited(); @@ -158,24 +161,23 @@ fn test_claim_tokens_once_then_claim_again() { address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 0, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 120, + max_x: 60, + max_y: 0, + }), }, ]; let vesting_client = instantiate_vesting_client(&env); - // admin has 120 vesting tokens prior to initializing the contract - assert_eq!(token_client.balance(&admin), 120); - - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); // after initialization the admin has 0 vesting tokens // contract has 120 vesting tokens @@ -189,7 +191,7 @@ fn test_claim_tokens_once_then_claim_again() { env.ledger().with_mut(|li| li.timestamp = 30); // user collects 1/2 of the vested tokens and transfers them to himself - vesting_client.transfer_token(&vester1, &vester1, &60); + vesting_client.claim(&vester1, &0); // vester1 has 60 tokens after claiming the vested amount assert_eq!(vesting_client.query_balance(&vester1), 60); @@ -201,7 +203,7 @@ fn test_claim_tokens_once_then_claim_again() { env.ledger().with_mut(|li| li.timestamp = 60); // user collects the remaining vested tokens and transfers them to himself - vesting_client.transfer_token(&vester1, &vester1, &60); + vesting_client.claim(&vester1, &0); // vester1 has 120 tokens after claiming the vested amount assert_eq!(vesting_client.query_balance(&vester1), 120); @@ -211,7 +213,7 @@ fn test_claim_tokens_once_then_claim_again() { } #[test] -fn test_user_can_claim_tokens_way_after_the_testing_period() { +fn user_can_claim_tokens_way_after_the_testing_period() { let env = Env::default(); env.mock_all_auths(); env.budget().reset_unlimited(); @@ -229,15 +231,16 @@ fn test_user_can_claim_tokens_way_after_the_testing_period() { address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 0, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 120, + max_x: 60, + max_y: 0, + }), }, ]; @@ -246,7 +249,8 @@ fn test_user_can_claim_tokens_way_after_the_testing_period() { // admin has 120 vesting tokens prior to initializing the contract assert_eq!(token_client.balance(&admin), 120); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); // after initialization the admin has 0 vesting tokens // contract has 120 vesting tokens @@ -260,90 +264,7 @@ fn test_user_can_claim_tokens_way_after_the_testing_period() { env.ledger().with_mut(|li| li.timestamp = 61); // user collects everything - vesting_client.transfer_token(&vester1, &vester1, &120); - - // vester1 has 120 tokens after claiming the vested amount - assert_eq!(vesting_client.query_balance(&vester1), 120); - - // there must be 0 vesting tokens left in the contract - assert_eq!(vesting_client.query_balance(&vesting_client.address), 0); -} - -#[test] -fn user_claims_only_a_part_of_the_allowed_vested_amount_then_claims_the_remaining_afterwards() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let token_client = deploy_token_contract(&env, &admin); - - token_client.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token_client.address.clone(), - }; - - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 0, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let vesting_client = instantiate_vesting_client(&env); - - // admin has 120 vesting tokens prior to initializing the contract - assert_eq!(token_client.balance(&admin), 120); - - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); - - // after initialization the admin has 0 vesting tokens - // contract has 120 vesting tokens - assert_eq!(token_client.balance(&admin), 0); - assert_eq!(token_client.balance(&vesting_client.address), 120); - - // vester1 has 0 tokens before claiming the vested amount - assert_eq!(vesting_client.query_balance(&vester1), 0); - - // we move time to the middle of the vesting period - env.ledger().with_mut(|li| li.timestamp = 30); - - // user can collect 30 tokens, but he only collects 15 - vesting_client.transfer_token(&vester1, &vester1, &15); - - // vester1 has 15 tokens after claiming the vested amount - assert_eq!(vesting_client.query_balance(&vester1), 15); - - // there must be 105 vesting tokens left in the contract - assert_eq!(vesting_client.query_balance(&vesting_client.address), 105); - - // we move the time to the end of the vesting period - env.ledger().with_mut(|li| li.timestamp = 60); - - // user collects 15 more tokens - vesting_client.transfer_token(&vester1, &vester1, &15); - - // vester1 has 30 tokens after claiming the vested amount - assert_eq!(vesting_client.query_balance(&vester1), 30); - - // there must be 90 vesting tokens left in the contract - assert_eq!(vesting_client.query_balance(&vesting_client.address), 90); - - // we move time way ahead in time - env.ledger().with_mut(|li| li.timestamp = 1000); - - // user decides it's times to become milionaire and collects the remaining 90 tokens - vesting_client.transfer_token(&vester1, &vester1, &90); + vesting_client.claim(&vester1, &0); // vester1 has 120 tokens after claiming the vested amount assert_eq!(vesting_client.query_balance(&vester1), 120); @@ -353,9 +274,7 @@ fn user_claims_only_a_part_of_the_allowed_vested_amount_then_claims_the_remainin } #[test] -#[should_panic( - expected = "Vesting: Verify Vesting Update Balances: Remaining amount must be at least equal to vested amount" -)] +#[should_panic(expected = "Vesting: Claim: No tokens available to claim")] fn transfer_vesting_token_before_vesting_period_starts_should_fail() { const START_TIMESTAMP: u64 = 15; let env = Env::default(); @@ -375,66 +294,77 @@ fn transfer_vesting_token_before_vesting_period_starts_should_fail() { address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: START_TIMESTAMP, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: START_TIMESTAMP, + min_y: 120, + max_x: 60, + max_y: 0, + }), }, ]; let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); // we set the timestamp at a time earlier than the vesting period start env.ledger() .with_mut(|li| li.timestamp = START_TIMESTAMP - 10); - // we try to transfer the tokens before the vesting period has started - vesting_client.transfer_token(&vester1, &vester1, &1); + // we try to claim the tokens before the vesting period has started + vesting_client.claim(&vester1, &0); } #[test] -#[should_panic(expected = "Vesting: Transfer token: Invalid transfer amount")] -fn transfer_tokens_should_fail_invalid_amount() { +#[should_panic(expected = "Vesting: Claim: No tokens available to claim")] +fn claim_after_all_tokens_have_been_claimed() { let env = Env::default(); env.mock_all_auths(); env.budget().reset_unlimited(); let admin = Address::generate(&env); let vester1 = Address::generate(&env); - let token_client = deploy_token_contract(&env, &admin); - token_client.mint(&admin, &120); + + token_client.mint(&admin, &1_000); let vesting_token = VestingTokenInfo { name: String::from_str(&env, "Phoenix"), symbol: String::from_str(&env, "PHO"), decimals: 6, - address: token_client.address, + address: token_client.address.clone(), }; - let vesting_balances = vec![ + + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 1_000, + max_x: 60, + max_y: 0, + }), }, ]; let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); + + env.ledger().with_mut(|li| li.timestamp = 61); - vesting_client.transfer_token(&vester1, &vester1, &0); + // we claim tokens once + vesting_client.claim(&vester1, &0); + assert_eq!(vesting_client.query_balance(&vester1), 1_000); + // and second one fails + vesting_client.claim(&vester1, &0); } #[test] @@ -458,45 +388,50 @@ fn transfer_works_with_multiple_users_and_distributions() { address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 0, - end_timestamp: 1_000, - amount: 300, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 300, + max_x: 1_000, + max_y: 0, + }), }, - VestingBalance { - rcpt_address: vester2.clone(), - distribution_info: DistributionInfo { - start_timestamp: 0, - end_timestamp: 500, - amount: 200, - }, + VestingSchedule { + recipient: vester2.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 200, + max_x: 500, + max_y: 0, + }), }, - VestingBalance { - rcpt_address: vester3.clone(), - distribution_info: DistributionInfo { - start_timestamp: 125, - end_timestamp: 750, - amount: 250, - }, + VestingSchedule { + recipient: vester3.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 125, + min_y: 250, + max_x: 750, + max_y: 0, + }), }, - VestingBalance { - rcpt_address: vester4.clone(), - distribution_info: DistributionInfo { - start_timestamp: 250, - end_timestamp: 1_500, - amount: 250, - }, + VestingSchedule { + recipient: vester4.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 250, + min_y: 250, + max_x: 1_500, + max_y: 0, + }), }, ]; let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); // vesting period for our 4 vesters is between 0 and 1_500 // we will move timestamp 3 times by 500 units and on each withdrawal we will transfer the vested amount @@ -505,22 +440,22 @@ fn transfer_works_with_multiple_users_and_distributions() { // vester1 can withdraw 150 tokens out of 300 tokens assert_eq!(vesting_client.query_balance(&vester1), 0); - vesting_client.transfer_token(&vester1, &vester1, &150); + vesting_client.claim(&vester1, &0); assert_eq!(vesting_client.query_balance(&vester1), 150); // vester2 can withdraw all tokens assert_eq!(vesting_client.query_balance(&vester2), 0); - vesting_client.transfer_token(&vester2, &vester2, &200); + vesting_client.claim(&vester2, &0); assert_eq!(vesting_client.query_balance(&vester2), 200); // vester3 can withdraw 150 tokens out of 250 tokens assert_eq!(vesting_client.query_balance(&vester3), 0); - vesting_client.transfer_token(&vester3, &vester3, &150); + vesting_client.claim(&vester3, &0); assert_eq!(vesting_client.query_balance(&vester3), 150); // vester4 can withdraw 50 tokens out of 250 tokens assert_eq!(vesting_client.query_balance(&vester4), 0); - vesting_client.transfer_token(&vester4, &vester4, &50); + vesting_client.claim(&vester4, &0); assert_eq!(vesting_client.query_balance(&vester4), 50); // users have withdrawn a total of 550 tokens @@ -532,18 +467,18 @@ fn transfer_works_with_multiple_users_and_distributions() { // vester1 can withdraw the remaining 150 tokens assert_eq!(vesting_client.query_balance(&vester1), 150); - vesting_client.transfer_token(&vester1, &vester1, &150); + vesting_client.claim(&vester1, &0); assert_eq!(vesting_client.query_balance(&vester1), 300); // vester2 has nothing to withdraw // vester3 can withdraw the remaining 100 tokens assert_eq!(vesting_client.query_balance(&vester3), 150); - vesting_client.transfer_token(&vester3, &vester3, &100); + vesting_client.claim(&vester3, &0); assert_eq!(vesting_client.query_balance(&vester3), 250); // vester4 can withdraw 100 - maximum for the period assert_eq!(vesting_client.query_balance(&vester4), 50); - vesting_client.transfer_token(&vester4, &vester4, &100); + vesting_client.claim(&vester4, &0); assert_eq!(vesting_client.query_balance(&vester4), 150); // in the 2nd round users have withdrawn 350 tokens @@ -557,7 +492,7 @@ fn transfer_works_with_multiple_users_and_distributions() { // vester3 has nothing to withdraw // vester4 can withdraw the remaining 100 tokens assert_eq!(vesting_client.query_balance(&vester4), 150); - vesting_client.transfer_token(&vester4, &vester4, &100); + vesting_client.claim(&vester4, &0); assert_eq!(vesting_client.query_balance(&vester4), 250); assert_eq!(vesting_client.query_balance(&vesting_client.address), 0); @@ -582,15 +517,16 @@ fn claim_works() { address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 0, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 120, + max_x: 60, + max_y: 0, + }), }, ]; @@ -599,7 +535,8 @@ fn claim_works() { // admin has 120 vesting tokens prior to initializing the contract assert_eq!(token_client.balance(&admin), 120); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); // after initialization the admin has 0 vesting tokens // contract has 120 vesting tokens @@ -609,18 +546,18 @@ fn claim_works() { // vester1 has 0 tokens before claiming the vested amount assert_eq!(vesting_client.query_balance(&vester1), 0); // vester1 has 0 tokens available for claiming before the vesting period starts - assert_eq!(vesting_client.query_available_to_claim(&vester1), 0); + assert_eq!(vesting_client.query_available_to_claim(&vester1, &0), 0); // we move time to half of the vesting period env.ledger().with_mut(|li| li.timestamp = 30); // vester1 claims all available for claiming tokens - vesting_client.claim(&vester1); + vesting_client.claim(&vester1, &0); // vester1 has 60 tokens after claiming the vested amount assert_eq!(vesting_client.query_balance(&vester1), 60); // vester1 has 0 tokens available for claiming after claiming the vested amount - assert_eq!(vesting_client.query_available_to_claim(&vester1), 0); + assert_eq!(vesting_client.query_available_to_claim(&vester1, &0), 0); // there must be 60 vesting tokens left in the contract - remaining for the 2nd vester assert_eq!(vesting_client.query_balance(&vesting_client.address), 60); @@ -629,13 +566,142 @@ fn claim_works() { env.ledger().with_mut(|li| li.timestamp = 60); // vester1 claims the remaining tokens - vesting_client.claim(&vester1); + vesting_client.claim(&vester1, &0); // vester1 has 120 tokens after claiming the vested amount assert_eq!(vesting_client.query_balance(&vester1), 120); // vester1 has 0 tokens available for claiming after claiming the vested amount - assert_eq!(vesting_client.query_available_to_claim(&vester1), 0); + assert_eq!(vesting_client.query_available_to_claim(&vester1, &0), 0); // there must be 0 vesting tokens left in the contract assert_eq!(vesting_client.query_balance(&vesting_client.address), 0); } + +#[test] +fn claim_tokens_from_two_distributions() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let vester1 = Address::generate(&env); + let token_client = deploy_token_contract(&env, &admin); + + token_client.mint(&admin, &2_000); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Phoenix"), + symbol: String::from_str(&env, "PHO"), + decimals: 6, + address: token_client.address.clone(), + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize(&admin, &vesting_token, &10u32); + + let vesting_schedules = vec![ + &env, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 1_500, + max_x: 100, + max_y: 0, + }), + }, + ]; + vesting_client.create_vesting_schedules(&vesting_schedules); + assert_eq!(token_client.balance(&vesting_client.address), 1_500); + + // vester1 has 0 tokens before claiming the vested amount + assert_eq!(vesting_client.query_balance(&vester1), 0); + + // we move time to the half of the vesting period + env.ledger().with_mut(|li| li.timestamp = 50); + + // user collects the vested tokens and transfers them to himself + vesting_client.claim(&vester1, &0); + + assert_eq!(vesting_client.query_balance(&vester1), 750); + assert_eq!(token_client.balance(&vesting_client.address), 750); + + // create a vesting schedule which starts in the middle of the previous one + let vesting_schedules = vec![ + &env, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::PiecewiseLinear(PiecewiseLinear { + steps: vec![ + &env, + Step { + time: 50, + value: 500, + }, + Step { + time: 100, + value: 250, + }, + Step { + time: 150, + value: 0, + }, + ], + }), + }, + ]; + vesting_client.create_vesting_schedules(&vesting_schedules); + + assert_eq!( + vesting_client.query_all_vesting_info(&vester1), + vec![ + &env, + VestingInfo { + recipient: vester1.clone(), + balance: 750, // balance is deducted because it was already once claimed + schedule: Curve::SaturatingLinear(SaturatingLinear { + min_x: 0, + min_y: 1_500, + max_x: 100, + max_y: 0, + }) + }, + VestingInfo { + recipient: vester1.clone(), + balance: 500, + schedule: Curve::PiecewiseLinear(PiecewiseLinear { + steps: vec![ + &env, + Step { + time: 50, + value: 500, + }, + Step { + time: 100, + value: 250, + }, + Step { + time: 150, + value: 0, + }, + ], + }) + } + ] + ); + // we move time to the half of the vesting period + env.ledger().with_mut(|li| li.timestamp = 100); + + vesting_client.claim(&vester1, &0); + assert_eq!(vesting_client.query_balance(&vester1), 1_500); + assert_eq!(token_client.balance(&vesting_client.address), 500); + + vesting_client.claim(&vester1, &1); + assert_eq!(vesting_client.query_balance(&vester1), 1_750); + assert_eq!(token_client.balance(&vesting_client.address), 250); + + env.ledger().with_mut(|li| li.timestamp = 150); + vesting_client.claim(&vester1, &1); + assert_eq!(vesting_client.query_balance(&vester1), 2_000); + assert_eq!(token_client.balance(&vesting_client.address), 0); +} diff --git a/contracts/vesting/src/tests/instantiate.rs b/contracts/vesting/src/tests/instantiate.rs index 77f05220..d799db09 100644 --- a/contracts/vesting/src/tests/instantiate.rs +++ b/contracts/vesting/src/tests/instantiate.rs @@ -1,9 +1,10 @@ use soroban_sdk::{testutils::Address as _, vec, Address, Env, String}; use crate::{ - storage::{DistributionInfo, MinterInfo, VestingBalance, VestingTokenInfo}, + storage::{VestingInfo, VestingSchedule, VestingTokenInfo}, tests::setup::{deploy_token_contract, instantiate_vesting_client}, }; +use curve::{Curve, SaturatingLinear}; #[test] fn instantiate_contract_successfully() { @@ -22,85 +23,55 @@ fn instantiate_contract_successfully() { decimals: 6, address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 15, + min_y: 120, + max_x: 60, + max_y: 0, + }), }, - VestingBalance { - rcpt_address: vester2, - distribution_info: DistributionInfo { - start_timestamp: 30, - end_timestamp: 120, - amount: 240, - }, + VestingSchedule { + recipient: vester2, + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 30, + min_y: 240, + max_x: 120, + max_y: 0, + }), }, ]; let vesting_client = instantiate_vesting_client(&env); token_client.mint(&admin, &480); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); assert_eq!(vesting_client.query_token_info(), vesting_token); assert_eq!( - vesting_client.query_distribution_info(&vester1), - vesting_balances.get(0).unwrap().distribution_info + vesting_client.query_all_vesting_info(&vester1), + vec![ + &env, + VestingInfo { + recipient: vester1, + balance: 120, + schedule: Curve::SaturatingLinear(SaturatingLinear { + min_x: 15, + min_y: 120, + max_x: 60, + max_y: 0, + }) + } + ] ); } -#[test] -fn instantiate_contract_successfully_with_constant_curve_minter_info() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let token_client = deploy_token_contract(&env, &admin); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token_client.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1, - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: Address::generate(&env), - mint_capacity: 511223344, - }; - - let vesting_client = instantiate_vesting_client(&env); - - token_client.mint(&admin, &240); - - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info), - &10u32, - ); - - assert_eq!(vesting_client.query_token_info(), vesting_token); -} - -#[should_panic(expected = "Vesting: Initialize: At least one vesting schedule must be provided.")] +#[should_panic( + expected = "Vesting: Create vesting account: At least one vesting schedule must be provided." +)] #[test] fn instantiate_contract_without_any_vesting_balances_should_fail() { let env = Env::default(); @@ -115,65 +86,20 @@ fn instantiate_contract_without_any_vesting_balances_should_fail() { decimals: 6, address: token_client.address.clone(), }; - let vesting_balances = vec![&env]; + let vesting_schedules = vec![&env]; let vesting_client = instantiate_vesting_client(&env); token_client.mint(&admin, &100); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); -} - -#[should_panic(expected = "Vesting: Initialize: total vested amount over the capacity")] -#[test] -fn instantiate_contract_should_panic_when_supply_over_the_cap() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - - let token_client = deploy_token_contract(&env, &admin); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token_client.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1, - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: Address::generate(&env), - mint_capacity: 100, - }; - - let vesting_client = instantiate_vesting_client(&env); - token_client.mint(&admin, &1_000); - - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info), - &10u32, - ); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); } #[should_panic( - expected = "Vesting: Initialize: Admin does not have enough tokens to start the vesting contract" + expected = "Vesting: Create vesting account: Admin does not have enough tokens to start the vesting schedule" )] #[test] -fn instantiate_contract_should_panic_when_admin_has_no_tokens_to_fund() { +fn create_schedule_panics_when_admin_has_no_tokens_to_fund() { let env = Env::default(); env.mock_all_auths(); @@ -188,19 +114,21 @@ fn instantiate_contract_should_panic_when_admin_has_no_tokens_to_fund() { decimals: 6, address: token_client.address.clone(), }; - let vesting_balances = vec![ + let vesting_schedules = vec![ &env, - VestingBalance { - rcpt_address: vester1, - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: vester1, + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 15, + min_y: 120, + max_x: 60, + max_y: 0, + }), }, ]; let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); + vesting_client.initialize(&admin, &vesting_token, &10u32); + vesting_client.create_vesting_schedules(&vesting_schedules); } diff --git a/contracts/vesting/src/tests/messages.rs b/contracts/vesting/src/tests/messages.rs deleted file mode 100644 index 4c8a058d..00000000 --- a/contracts/vesting/src/tests/messages.rs +++ /dev/null @@ -1,724 +0,0 @@ -use soroban_sdk::{ - testutils::{Address as _, Ledger}, - vec, Address, Env, String, -}; - -use crate::storage::{DistributionInfo, MinterInfo, VestingBalance, VestingTokenInfo}; - -use super::setup::{deploy_token_contract, instantiate_vesting_client}; - -#[test] -fn burn_works() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let token = deploy_token_contract(&env, &admin); - - token.mint(&admin, &1_000); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); - - env.ledger().with_mut(|li| li.timestamp = 100); - assert_eq!(vesting_client.query_vesting_contract_balance(), 120); - - vesting_client.transfer_token(&vester1, &vester1, &120); - assert_eq!(vesting_client.query_vesting_contract_balance(), 0); - assert_eq!(token.balance(&vester1), 120); - - vesting_client.burn(&vester1, &120); - assert_eq!(token.balance(&vester1), 0); -} - -#[test] -#[should_panic(expected = "Vesting: Burn: Invalid burn amount")] -fn burn_should_panic_when_invalid_amount() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let token = deploy_token_contract(&env, &admin); - - token.mint(&admin, &1_000); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); - - vesting_client.burn(&vester1, &0); -} - -#[test] -fn mint_works() { - let env = Env::default(); - env.mock_all_auths_allowing_non_root_auth(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let minter = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: minter.clone(), - mint_capacity: 500, - }; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info.clone()), - &10u32, - ); - - // we start with 120 tokens minted to the contract - assert_eq!(vesting_client.query_vesting_contract_balance(), 120); - // amdin should have none - assert_eq!(token.balance(&admin), 0); - - // minter can mint up to 500 tokens - assert_eq!(vesting_client.query_minter().mint_capacity, 500); - - // user withdraws 120 tokens - env.ledger().with_mut(|li| li.timestamp = 100); - vesting_client.transfer_token(&vester1, &vester1, &120); - assert_eq!(token.balance(&vester1), 120); - assert_eq!(vesting_client.query_vesting_contract_balance(), 0); - - // minter decides to mint new 250 tokens - vesting_client.mint(&minter, &250); - assert_eq!(vesting_client.query_vesting_contract_balance(), 250); - assert_eq!(vesting_client.query_minter().mint_capacity, 250); - - // we mint 250 more tokens - vesting_client.mint(&minter, &250); - assert_eq!(vesting_client.query_vesting_contract_balance(), 500); - assert_eq!(vesting_client.query_minter().mint_capacity, 0); -} - -#[test] -#[should_panic(expected = "Vesting: Mint: Invalid mint amount")] -fn mint_should_panic_when_invalid_amount() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: vester1.clone(), - mint_capacity: 500, - }; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info), - &10u32, - ); - - vesting_client.mint(&vester1, &0); -} - -#[test] -#[should_panic(expected = "Vesting: Mint: Not authorized to mint")] -fn mint_should_panic_when_not_authorized_to_mint() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: Address::generate(&env), - mint_capacity: 500, - }; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info), - &10u32, - ); - - vesting_client.mint(&vester1, &100); -} - -#[test] -#[should_panic(expected = "Vesting: Mint: Minter does not have enough capacity to mint")] -fn mint_should_panic_when_mintet_does_not_have_enough_capacity() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let minter = Address::generate(&env); - let vester1 = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: minter.clone(), - mint_capacity: 500, - }; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info), - &10u32, - ); - - vesting_client.mint(&minter, &1_500); -} - -#[test] -fn update_minter_works_correctly() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let minter = Address::generate(&env); - let new_minter = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: minter.clone(), - mint_capacity: 500, - }; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info.clone()), - &10u32, - ); - - assert_eq!(vesting_client.query_minter(), minter_info); - - let new_minter_info = MinterInfo { - address: new_minter.clone(), - mint_capacity: 1_000, - }; - - vesting_client.update_minter(&minter, &new_minter_info.address); - - assert_eq!( - vesting_client.query_minter().address, - new_minter_info.address - ); -} - -#[test] -fn update_minter_works_correctly_when_no_minter_was_set_initially() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let new_minter = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); - - let new_minter_info = MinterInfo { - address: new_minter.clone(), - mint_capacity: 1_000, - }; - - vesting_client.update_minter(&admin, &new_minter_info.address); - - assert_eq!( - vesting_client.query_minter().address, - new_minter_info.address - ); -} - -#[test] -#[should_panic(expected = "Vesting: Update minter: Not authorized to update minter")] -fn update_minter_fails_when_not_authorized() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let new_minter = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: Address::generate(&env), - mint_capacity: 500, - }; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info.clone()), - &10u32, - ); - - let new_minter_info = MinterInfo { - address: new_minter.clone(), - mint_capacity: 1_000, - }; - - vesting_client.update_minter(&Address::generate(&env), &new_minter_info.address); -} - -#[test] -#[should_panic(expected = "Vesting: Mint: Minter not found")] -fn minting_fails_because_no_minter_was_found() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); - - vesting_client.mint(&admin, &500); -} - -#[test] -#[should_panic(expected = "Vesting: Update Minter Capacity: Minter not found")] -fn update_minter_capacity_fails_because_no_minter_found() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); - - vesting_client.update_minter_capacity(&admin, &500); -} - -#[test] -#[should_panic(expected = "Vesting: Query Minter: Minter not found")] -fn query_minter_should_fail_because_no_minter_found() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); - - vesting_client.query_minter(); -} - -#[test] -fn test_should_update_minter_capacity_when_replacing_old_capacity() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let minter = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: minter.clone(), - mint_capacity: 500, - }; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info.clone()), - &10u32, - ); - - let new_minter_capacity = 1_000; - vesting_client.update_minter_capacity(&admin, &new_minter_capacity); - - assert_eq!( - vesting_client.query_minter().mint_capacity, - new_minter_capacity - ); -} - -#[test] -#[should_panic( - expected = "Vesting: Update minter capacity: Only contract's admin can update the minter's capacity" -)] -fn test_should_panic_when_updating_minter_capacity_without_auth() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - let minter = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let minter_info = MinterInfo { - address: minter, - mint_capacity: 500, - }; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize( - &admin, - &vesting_token, - &vesting_balances, - &Some(minter_info.clone()), - &10u32, - ); - - vesting_client.update_minter_capacity(&Address::generate(&env), &1_000); -} - -#[test] -#[should_panic(expected = "zero balance is not sufficient to spend")] -fn test_should_fail_when_burning_more_than_the_user_has() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let vester1 = Address::generate(&env); - - let token = deploy_token_contract(&env, &admin); - token.mint(&admin, &120); - - let vesting_token = VestingTokenInfo { - name: String::from_str(&env, "Phoenix"), - symbol: String::from_str(&env, "PHO"), - decimals: 6, - address: token.address.clone(), - }; - - let vesting_balances = vec![ - &env, - VestingBalance { - rcpt_address: vester1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, - }, - ]; - - let vesting_client = instantiate_vesting_client(&env); - vesting_client.initialize(&admin, &vesting_token, &vesting_balances, &None, &10u32); - - // vester1 has 0 tokens - assert_eq!(token.balance(&vester1), 0); - // vester1 tries to burn 121 tokens - vesting_client.burn(&vester1, &121); -} diff --git a/contracts/vesting/src/tests/minter.rs b/contracts/vesting/src/tests/minter.rs new file mode 100644 index 00000000..8d38e6ee --- /dev/null +++ b/contracts/vesting/src/tests/minter.rs @@ -0,0 +1,480 @@ +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + vec, Address, Env, String, +}; + +use crate::storage::{MinterInfo, VestingSchedule, VestingTokenInfo}; +use curve::{Curve, SaturatingLinear}; + +use super::setup::{deploy_token_contract, instantiate_vesting_client}; + +#[test] +fn instantiate_contract_successfully_with_constant_curve_minter_info() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_client = deploy_token_contract(&env, &admin); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Phoenix"), + symbol: String::from_str(&env, "PHO"), + decimals: 6, + address: token_client.address.clone(), + }; + + let minter_info = MinterInfo { + address: Address::generate(&env), + mint_capacity: 511223344, + }; + + let vesting_client = instantiate_vesting_client(&env); + + token_client.mint(&admin, &240); + + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info); + + assert_eq!(vesting_client.query_token_info(), vesting_token); +} + +#[should_panic(expected = "Vesting: Mint: Minter does not have enough capacity to mint")] +#[test] +fn mint_panics_when_over_the_cap() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Phoenix"), + symbol: String::from_str(&env, "PHO"), + decimals: 6, + address: token_client.address.clone(), + }; + + let minter = Address::generate(&env); + let minter_info = MinterInfo { + address: minter.clone(), + mint_capacity: 100, + }; + + let vesting_client = instantiate_vesting_client(&env); + + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info); + vesting_client.mint(&minter, &110i128); +} + +#[test] +fn burn_works() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let vester1 = Address::generate(&env); + let token = deploy_token_contract(&env, &admin); + + token.mint(&admin, &1_000); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + let vesting_schedules = vec![ + &env, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 15, + min_y: 120, + max_x: 60, + max_y: 0, + }), + }, + ]; + + let vesting_client = instantiate_vesting_client(&env); + let minter_info = MinterInfo { + address: Address::generate(&env), + mint_capacity: 10_000, + }; + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info); + vesting_client.create_vesting_schedules(&vesting_schedules); + + env.ledger().with_mut(|li| li.timestamp = 100); + assert_eq!(vesting_client.query_vesting_contract_balance(), 120); + + vesting_client.claim(&vester1, &0); + assert_eq!(vesting_client.query_vesting_contract_balance(), 0); + assert_eq!(token.balance(&vester1), 120); + + vesting_client.burn(&vester1, &120); + assert_eq!(token.balance(&vester1), 0); +} + +#[test] +#[should_panic(expected = "Vesting: Burn: Invalid burn amount")] +fn burn_should_panic_when_invalid_amount() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let token = deploy_token_contract(&env, &admin); + + token.mint(&admin, &1_000); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let vesting_client = instantiate_vesting_client(&env); + let minter_info = MinterInfo { + address: Address::generate(&env), + mint_capacity: 10_000, + }; + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info); + vesting_client.burn(&Address::generate(&env), &0); +} + +#[test] +fn mint_works() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let vester1 = Address::generate(&env); + let minter = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + let vesting_schedules = vec![ + &env, + VestingSchedule { + recipient: vester1.clone(), + curve: Curve::SaturatingLinear(SaturatingLinear { + min_x: 15, + min_y: 120, + max_x: 60, + max_y: 0, + }), + }, + ]; + + let minter_info = MinterInfo { + address: minter.clone(), + mint_capacity: 500, + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info.clone()); + vesting_client.create_vesting_schedules(&vesting_schedules); + + // we start with 120 tokens minted to the contract + assert_eq!(vesting_client.query_vesting_contract_balance(), 120); + // amdin should have none + assert_eq!(token.balance(&admin), 0); + + // minter can mint up to 500 tokens + assert_eq!(vesting_client.query_minter().mint_capacity, 500); + + // user withdraws 120 tokens + env.ledger().with_mut(|li| li.timestamp = 100); + vesting_client.claim(&vester1, &0); + assert_eq!(token.balance(&vester1), 120); + assert_eq!(vesting_client.query_vesting_contract_balance(), 0); + + // minter decides to mint new 250 tokens + vesting_client.mint(&minter, &250); + assert_eq!(vesting_client.query_vesting_contract_balance(), 250); + assert_eq!(vesting_client.query_minter().mint_capacity, 250); + + // we mint 250 more tokens + vesting_client.mint(&minter, &250); + assert_eq!(vesting_client.query_vesting_contract_balance(), 500); + assert_eq!(vesting_client.query_minter().mint_capacity, 0); +} + +#[test] +#[should_panic(expected = "Vesting: Mint: Invalid mint amount")] +fn mint_should_panic_when_invalid_amount() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let minter_info = MinterInfo { + address: admin.clone(), + mint_capacity: 500, + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info); + + vesting_client.mint(&Address::generate(&env), &0); +} + +#[test] +#[should_panic(expected = "Vesting: Mint: Not authorized to mint")] +fn mint_should_panic_when_not_authorized_to_mint() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let vester1 = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let minter_info = MinterInfo { + address: Address::generate(&env), + mint_capacity: 500, + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info); + + vesting_client.mint(&vester1, &100); +} + +#[test] +#[should_panic(expected = "Vesting: Mint: Minter does not have enough capacity to mint")] +fn mint_should_panic_when_mintet_does_not_have_enough_capacity() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let minter = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let minter_info = MinterInfo { + address: minter.clone(), + mint_capacity: 500, + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info); + + vesting_client.mint(&minter, &1_500); +} + +#[test] +fn update_minter_works_correctly() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let minter = Address::generate(&env); + let new_minter = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let minter_info = MinterInfo { + address: minter.clone(), + mint_capacity: 500, + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info.clone()); + + assert_eq!(vesting_client.query_minter(), minter_info); + + let new_minter_info = MinterInfo { + address: new_minter.clone(), + mint_capacity: 1_000, + }; + + vesting_client.update_minter(&minter, &new_minter_info.address); + + assert_eq!( + vesting_client.query_minter().address, + new_minter_info.address + ); +} + +#[test] +#[should_panic(expected = "Vesting: Update minter: Not authorized to update minter")] +fn update_minter_fails_when_not_authorized() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let new_minter = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let minter_info = MinterInfo { + address: Address::generate(&env), + mint_capacity: 500, + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info.clone()); + + let new_minter_info = MinterInfo { + address: new_minter.clone(), + mint_capacity: 1_000, + }; + + vesting_client.update_minter(&Address::generate(&env), &new_minter_info.address); +} + +#[test] +fn update_minter_capacity_when_replacing_old_capacity() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let minter = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let minter_info = MinterInfo { + address: minter.clone(), + mint_capacity: 50_000, + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info.clone()); + + let new_minter_capacity = 1_000; + vesting_client.update_minter_capacity(&admin, &new_minter_capacity); + + assert_eq!( + vesting_client.query_minter().mint_capacity, + new_minter_capacity + ); +} + +#[test] +#[should_panic( + expected = "Vesting: Update minter capacity: Only contract's admin can update the minter's capacity" +)] +fn updating_minter_capacity_without_auth() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let minter = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let minter_info = MinterInfo { + address: minter, + mint_capacity: 50_000, + }; + + let vesting_client = instantiate_vesting_client(&env); + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info.clone()); + + vesting_client.update_minter_capacity(&Address::generate(&env), &1_000); +} + +#[test] +#[should_panic(expected = "zero balance is not sufficient to spend")] +fn burning_more_than_balance() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&admin, &120); + + let vesting_token = VestingTokenInfo { + name: String::from_str(&env, "Token"), + symbol: String::from_str(&env, "TOK"), + decimals: 6, + address: token.address.clone(), + }; + + let vesting_client = instantiate_vesting_client(&env); + let minter_info = MinterInfo { + address: Address::generate(&env), + mint_capacity: 1_000, + }; + vesting_client.initialize_with_minter(&admin, &vesting_token, &10u32, &minter_info); + + // vester1 tries to burn 121 tokens + vesting_client.burn(&Address::generate(&env), &121); +} diff --git a/contracts/vesting/src/utils.rs b/contracts/vesting/src/utils.rs index 742850be..437c3e88 100644 --- a/contracts/vesting/src/utils.rs +++ b/contracts/vesting/src/utils.rs @@ -1,131 +1,70 @@ use curve::Curve; use soroban_sdk::{log, panic_with_error, Address, Env, Vec}; -use crate::{ - error::ContractError, - storage::{get_vesting, save_vesting, VestingBalance, VestingInfo}, -}; - -pub fn verify_vesting_and_update_balances(env: &Env, sender: &Address, amount: u128) { - let vesting_info = get_vesting(env, sender); - let vested = vesting_info - .distribution_info - .get_curve() - .value(env.ledger().timestamp()); - - let sender_balance = vesting_info.balance; - let sender_liquid = sender_balance // this checks if we can withdraw any vesting - .checked_sub(vested) - .unwrap_or_else(|| panic_with_error!(env, ContractError::NotEnoughBalance)); - - if sender_liquid < amount { - log!( - &env, - "Vesting: Verify Vesting Update Balances: Remaining amount must be at least equal to vested amount" - ); - panic_with_error!(env, ContractError::CantMoveVestingTokens); - } +use crate::{error::ContractError, storage::VestingSchedule}; - save_vesting( - env, - sender, - &VestingInfo { - balance: sender_balance - amount, - distribution_info: vesting_info.distribution_info, - }, - ); +pub fn check_duplications(env: &Env, accounts: Vec) { + let mut addresses: Vec
= Vec::new(env); + for account in accounts.iter() { + if addresses.contains(&account.recipient) { + log!(&env, "Vesting: Initialize: Duplicate addresses found"); + panic_with_error!(env, ContractError::DuplicateInitialBalanceAddresses); + } + addresses.push_back(account.recipient.clone()); + } } -pub fn create_vesting_accounts( - env: &Env, - vesting_complexity: u32, - vesting_accounts: Vec, -) -> u128 { - validate_accounts(env, vesting_accounts.clone()); - - let mut total_vested_amount = 0; - - vesting_accounts.into_iter().for_each(|vb| { - assert_schedule_vests_amount( - env, - &vb.distribution_info.get_curve(), - vb.distribution_info.amount, - ) - .expect("Invalid curve and amount"); - - if vesting_complexity <= vb.distribution_info.get_curve().size() { +/// Asserts the vesting schedule decreases to 0 eventually +/// returns the total vested amount +pub fn validate_vesting_schedule(env: &Env, schedule: &Curve) -> Result { + schedule.validate_monotonic_decreasing()?; + match schedule { + Curve::Constant(_) => { log!( &env, - "Vesting: Create vesting account: Invalid curve complexity for {}", - vb.rcpt_address + "Vesting: Constant curve is not valid for a vesting schedule" ); - panic_with_error!(env, ContractError::VestingComplexityTooHigh); + panic_with_error!(&env, ContractError::CurveConstant) } - - save_vesting( - env, - &vb.rcpt_address, - &VestingInfo { - balance: vb.distribution_info.amount, - distribution_info: vb.distribution_info.clone(), - }, - ); - - total_vested_amount += vb.distribution_info.amount; - }); - - total_vested_amount -} - -/// Asserts the vesting schedule decreases to 0 eventually, and is never more than the -/// amount being sent. If it doesn't match these conditions, returns an error. -pub fn assert_schedule_vests_amount( - env: &Env, - schedule: &Curve, - amount: u128, -) -> Result<(), ContractError> { - schedule.validate_monotonic_decreasing()?; - let (low, high) = schedule.range(); - if low != 0 { - log!( - &env, - "Vesting: Transfer Vesting: Cannot transfer when non-fully vested" - ); - panic_with_error!(&env, ContractError::NeverFullyVested) - } else if high > amount { - log!( - &env, - "Vesting: Assert Schedule Vest Amount: Vesting amount more than sent" - ); - panic_with_error!(&env, ContractError::VestsMoreThanSent) - } else { - Ok(()) - } -} - -fn validate_accounts(env: &Env, accounts: Vec) { - let mut addresses: Vec
= Vec::new(env); - for account in accounts.iter() { - if addresses.contains(&account.rcpt_address) { - log!(&env, "Vesting: Initialize: Duplicate addresses found"); - panic_with_error!(env, ContractError::DuplicateInitialBalanceAddresses); + Curve::SaturatingLinear(sl) => { + // Check range + let (low, high) = (sl.max_y, sl.min_y); + if low != 0 { + log!( + &env, + "Vesting: Transfer Vesting: Cannot transfer when non-fully vested" + ); + panic_with_error!(&env, ContractError::NeverFullyVested) + } else { + Ok(high) // return the total amount to be transferred + } + } + Curve::PiecewiseLinear(pl) => { + // Check the last step value + if pl.end_value().unwrap() != 0 { + log!( + &env, + "Vesting: Transfer Vesting: Cannot transfer when non-fully vested" + ); + panic_with_error!(&env, ContractError::NeverFullyVested) + } + + // Return the amount to be distributed (value of the first step) + Ok(pl.first_value().unwrap()) } - addresses.push_back(account.rcpt_address.clone()); } } #[cfg(test)] mod test { - use curve::SaturatingLinear; + use curve::{PiecewiseLinear, SaturatingLinear, Step}; use soroban_sdk::testutils::Address as _; use soroban_sdk::vec; - use crate::storage::DistributionInfo; - use super::*; #[test] - fn validate_accounts_works() { + fn check_duplications_works() { let env = Env::default(); let address1 = Address::generate(&env); let address2 = Address::generate(&env); @@ -133,74 +72,50 @@ mod test { let accounts = vec![ &env, - VestingBalance { - rcpt_address: address1.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: address1.clone(), + curve: Curve::Constant(1), }, - VestingBalance { - rcpt_address: address2.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: address2.clone(), + curve: Curve::Constant(1), }, - VestingBalance { - rcpt_address: address3.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: address3.clone(), + curve: Curve::Constant(1), }, ]; // not panicking should be enough to pass the test - validate_accounts(&env, accounts); + check_duplications(&env, accounts); } #[test] #[should_panic(expected = "Vesting: Initialize: Duplicate addresses found")] - fn validate_accounts_should_panic() { + fn check_duplications_should_panic() { let env = Env::default(); let duplicate_address = Address::generate(&env); let accounts = vec![ &env, - VestingBalance { - rcpt_address: duplicate_address.clone(), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: duplicate_address.clone(), + curve: Curve::Constant(1), }, - VestingBalance { - rcpt_address: duplicate_address, - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: Address::generate(&env), + curve: Curve::Constant(1), }, - VestingBalance { - rcpt_address: Address::generate(&env), - distribution_info: DistributionInfo { - start_timestamp: 15, - end_timestamp: 60, - amount: 120, - }, + VestingSchedule { + recipient: duplicate_address, + curve: Curve::Constant(1), }, ]; - validate_accounts(&env, accounts); + check_duplications(&env, accounts); } #[test] - fn assert_schedule_vests_amount_works() { + fn validate_saturating_linear_vesting() { let env = Env::default(); let curve = Curve::SaturatingLinear(SaturatingLinear { min_x: 15, @@ -209,39 +124,61 @@ mod test { max_y: 0, }); - assert_eq!(assert_schedule_vests_amount(&env, &curve, 121), Ok(())); + assert_eq!(validate_vesting_schedule(&env, &curve), Ok(120)); + } + + #[test] + fn validate_piecewise_linear_vesting() { + let env = Env::default(); + let curve = Curve::PiecewiseLinear(PiecewiseLinear { + steps: vec![ + &env, + Step { + time: 60, + value: 150, + }, + Step { + time: 120, + value: 0, + }, + ], + }); + + assert_eq!(validate_vesting_schedule(&env, &curve), Ok(150)); } #[test] #[should_panic(expected = "Vesting: Transfer Vesting: Cannot transfer when non-fully vested")] - fn assert_schedule_vests_amount_fails_when_low_not_zero() { - const MIN_NOT_ZERO: u128 = 1; + fn saturating_linear_schedule_fails_when_not_fully_vested() { let env = Env::default(); let curve = Curve::SaturatingLinear(SaturatingLinear { min_x: 15, min_y: 120, max_x: 60, - max_y: MIN_NOT_ZERO, + max_y: 1, // leave 1 token at the end }); - assert_schedule_vests_amount(&env, &curve, 1_000).unwrap(); + validate_vesting_schedule(&env, &curve).unwrap(); } #[test] - #[should_panic( - expected = "Vesting: Assert Schedule Vest Amount: Vesting amount more than sent" - )] - fn assert_schedule_vests_amount_fails_when_high_bigger_than_amount() { - const HIGH: u128 = 2; - const AMOUNT: u128 = 1; + #[should_panic(expected = "Vesting: Transfer Vesting: Cannot transfer when non-fully vested")] + fn piecewise_linear_schedule_fails_when_not_fully_vested() { let env = Env::default(); - let curve = Curve::SaturatingLinear(SaturatingLinear { - min_x: 15, - min_y: HIGH, - max_x: 60, - max_y: 0, + let curve = Curve::PiecewiseLinear(PiecewiseLinear { + steps: vec![ + &env, + Step { + time: 60, + value: 120, + }, + Step { + time: 120, + value: 10, + }, + ], }); - assert_schedule_vests_amount(&env, &curve, AMOUNT).unwrap(); + validate_vesting_schedule(&env, &curve).unwrap(); } } diff --git a/packages/curve/src/lib.rs b/packages/curve/src/lib.rs index c071c035..f5be86eb 100644 --- a/packages/curve/src/lib.rs +++ b/packages/curve/src/lib.rs @@ -284,8 +284,8 @@ fn interpolate((min_x, min_y): (u64, u128), (max_x, max_y): (u64, u128), x: u64) #[contracttype] #[derive(Debug, Clone, Eq, PartialEq)] pub struct Step { - time: u64, - value: u128, + pub time: u64, + pub value: u128, } #[contracttype] @@ -472,6 +472,14 @@ impl PiecewiseLinear { fn end(&self) -> Option { self.steps.last().map(|Step { time, value: _ }| time) } + + pub fn end_value(&self) -> Option { + self.steps.last().map(|Step { time: _, value }| value) + } + + pub fn first_value(&self) -> Option { + self.steps.first().map(|Step { time: _, value }| value) + } } pub fn from_saturating_linear(env: &Env, sl: &SaturatingLinear) -> PiecewiseLinear {