684 lines
21 KiB
Rust
684 lines
21 KiB
Rust
//! Token vesting contracts for Synor.
|
|
//!
|
|
//! Implements token lockup with:
|
|
//! - Cliff period (no tokens released)
|
|
//! - Linear vesting (gradual release after cliff)
|
|
//! - Revocation support (for employee departures)
|
|
//!
|
|
//! # Example Vesting Schedule
|
|
//!
|
|
//! ```text
|
|
//! 4-year vesting with 1-year cliff:
|
|
//!
|
|
//! Tokens
|
|
//! Released
|
|
//! │
|
|
//! 100%│ ●────────
|
|
//! │ ●───●
|
|
//! │ ●───●
|
|
//! │ ●───●
|
|
//! │ ●───●
|
|
//! 25%│ ●───●
|
|
//! │ ●────●
|
|
//! 0%├─────●
|
|
//! └──────┬──────┬──────┬──────┬──────► Time
|
|
//! 1yr 2yr 3yr 4yr
|
|
//! (cliff)
|
|
//! ```
|
|
|
|
use borsh::{BorshDeserialize, BorshSerialize};
|
|
use serde::{Deserialize, Serialize};
|
|
use synor_types::{Address, Hash256};
|
|
use thiserror::Error;
|
|
|
|
/// Vesting schedule parameters.
|
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
|
|
pub struct VestingSchedule {
|
|
/// Total amount of tokens to vest.
|
|
pub total_amount: u64,
|
|
/// Cliff duration in seconds.
|
|
pub cliff_duration: u64,
|
|
/// Total vesting duration in seconds (including cliff).
|
|
pub vesting_duration: u64,
|
|
/// Start timestamp (Unix seconds).
|
|
pub start_time: u64,
|
|
/// Whether the schedule is revocable by the grantor.
|
|
pub revocable: bool,
|
|
}
|
|
|
|
impl VestingSchedule {
|
|
/// Creates a new vesting schedule.
|
|
pub fn new(
|
|
total_amount: u64,
|
|
cliff_months: u32,
|
|
vesting_months: u32,
|
|
start_time: u64,
|
|
revocable: bool,
|
|
) -> Self {
|
|
let seconds_per_month = 30 * 24 * 60 * 60; // ~30 days
|
|
VestingSchedule {
|
|
total_amount,
|
|
cliff_duration: cliff_months as u64 * seconds_per_month,
|
|
vesting_duration: vesting_months as u64 * seconds_per_month,
|
|
start_time,
|
|
revocable,
|
|
}
|
|
}
|
|
|
|
/// Standard 4-year vesting with 1-year cliff.
|
|
pub fn standard_4year(total_amount: u64, start_time: u64) -> Self {
|
|
Self::new(total_amount, 12, 48, start_time, true)
|
|
}
|
|
|
|
/// Advisor vesting (2-year with 6-month cliff).
|
|
pub fn advisor(total_amount: u64, start_time: u64) -> Self {
|
|
Self::new(total_amount, 6, 24, start_time, true)
|
|
}
|
|
|
|
/// Investor vesting (1-year linear, no cliff).
|
|
pub fn investor(total_amount: u64, start_time: u64) -> Self {
|
|
Self::new(total_amount, 0, 12, start_time, false)
|
|
}
|
|
|
|
/// Founder vesting (4-year with 1-year cliff, non-revocable).
|
|
pub fn founder(total_amount: u64, start_time: u64) -> Self {
|
|
Self::new(total_amount, 12, 48, start_time, false)
|
|
}
|
|
|
|
/// Calculates vested amount at a given timestamp.
|
|
pub fn vested_at(&self, timestamp: u64) -> u64 {
|
|
if timestamp < self.start_time {
|
|
return 0;
|
|
}
|
|
|
|
let elapsed = timestamp - self.start_time;
|
|
|
|
// Before cliff: nothing vested
|
|
if elapsed < self.cliff_duration {
|
|
return 0;
|
|
}
|
|
|
|
// After full vesting: everything vested
|
|
if elapsed >= self.vesting_duration {
|
|
return self.total_amount;
|
|
}
|
|
|
|
// Linear vesting between cliff and end
|
|
// vested = total * elapsed / vesting_duration
|
|
(self.total_amount as u128 * elapsed as u128 / self.vesting_duration as u128) as u64
|
|
}
|
|
|
|
/// Returns the cliff end timestamp.
|
|
pub fn cliff_end(&self) -> u64 {
|
|
self.start_time + self.cliff_duration
|
|
}
|
|
|
|
/// Returns the vesting end timestamp.
|
|
pub fn vesting_end(&self) -> u64 {
|
|
self.start_time + self.vesting_duration
|
|
}
|
|
|
|
/// Checks if the cliff has passed.
|
|
pub fn cliff_passed(&self, timestamp: u64) -> bool {
|
|
timestamp >= self.cliff_end()
|
|
}
|
|
|
|
/// Checks if vesting is complete.
|
|
pub fn fully_vested(&self, timestamp: u64) -> bool {
|
|
timestamp >= self.vesting_end()
|
|
}
|
|
}
|
|
|
|
/// Current state of a vesting contract.
|
|
#[derive(
|
|
Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize,
|
|
)]
|
|
pub enum VestingState {
|
|
/// Vesting is active.
|
|
Active,
|
|
/// Vesting was revoked by grantor.
|
|
Revoked,
|
|
/// All tokens have been claimed.
|
|
Completed,
|
|
}
|
|
|
|
/// A vesting contract instance.
|
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
|
|
pub struct VestingContract {
|
|
/// Unique identifier (hash of creation params).
|
|
pub id: Hash256,
|
|
/// Beneficiary who receives the tokens.
|
|
pub beneficiary: Address,
|
|
/// Grantor who created the vesting (can revoke if revocable).
|
|
pub grantor: Address,
|
|
/// Vesting schedule parameters.
|
|
pub schedule: VestingSchedule,
|
|
/// Amount already claimed by beneficiary.
|
|
pub claimed_amount: u64,
|
|
/// Current state.
|
|
pub state: VestingState,
|
|
/// Timestamp when revoked (if applicable).
|
|
pub revoked_at: Option<u64>,
|
|
/// Description/purpose of this vesting.
|
|
pub description: String,
|
|
}
|
|
|
|
/// Vesting errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum VestingError {
|
|
#[error("Vesting contract not found")]
|
|
NotFound,
|
|
|
|
#[error("Not authorized to perform this action")]
|
|
Unauthorized,
|
|
|
|
#[error("Nothing available to claim")]
|
|
NothingToClaim,
|
|
|
|
#[error("Vesting is not revocable")]
|
|
NotRevocable,
|
|
|
|
#[error("Vesting already revoked")]
|
|
AlreadyRevoked,
|
|
|
|
#[error("Vesting already completed")]
|
|
AlreadyCompleted,
|
|
|
|
#[error("Invalid vesting parameters")]
|
|
InvalidParameters,
|
|
|
|
#[error("Cliff not yet passed")]
|
|
CliffNotPassed,
|
|
|
|
#[error("Insufficient balance")]
|
|
InsufficientBalance,
|
|
}
|
|
|
|
impl VestingContract {
|
|
/// Creates a new vesting contract.
|
|
pub fn new(
|
|
beneficiary: Address,
|
|
grantor: Address,
|
|
schedule: VestingSchedule,
|
|
description: String,
|
|
) -> Result<Self, VestingError> {
|
|
// Validate parameters
|
|
if schedule.total_amount == 0 {
|
|
return Err(VestingError::InvalidParameters);
|
|
}
|
|
if schedule.vesting_duration == 0 {
|
|
return Err(VestingError::InvalidParameters);
|
|
}
|
|
if schedule.cliff_duration > schedule.vesting_duration {
|
|
return Err(VestingError::InvalidParameters);
|
|
}
|
|
|
|
// Generate unique ID
|
|
let mut id_data = Vec::new();
|
|
id_data.extend_from_slice(beneficiary.payload());
|
|
id_data.extend_from_slice(grantor.payload());
|
|
id_data.extend_from_slice(&schedule.start_time.to_le_bytes());
|
|
id_data.extend_from_slice(&schedule.total_amount.to_le_bytes());
|
|
let id = Hash256::blake3(&id_data);
|
|
|
|
Ok(VestingContract {
|
|
id,
|
|
beneficiary,
|
|
grantor,
|
|
schedule,
|
|
claimed_amount: 0,
|
|
state: VestingState::Active,
|
|
revoked_at: None,
|
|
description,
|
|
})
|
|
}
|
|
|
|
/// Creates a vesting contract for a team member.
|
|
pub fn for_team_member(
|
|
beneficiary: Address,
|
|
grantor: Address,
|
|
amount: u64,
|
|
start_time: u64,
|
|
name: &str,
|
|
) -> Result<Self, VestingError> {
|
|
Self::new(
|
|
beneficiary,
|
|
grantor,
|
|
VestingSchedule::standard_4year(amount, start_time),
|
|
format!("Team allocation for {}", name),
|
|
)
|
|
}
|
|
|
|
/// Creates a vesting contract for an advisor.
|
|
pub fn for_advisor(
|
|
beneficiary: Address,
|
|
grantor: Address,
|
|
amount: u64,
|
|
start_time: u64,
|
|
name: &str,
|
|
) -> Result<Self, VestingError> {
|
|
Self::new(
|
|
beneficiary,
|
|
grantor,
|
|
VestingSchedule::advisor(amount, start_time),
|
|
format!("Advisor allocation for {}", name),
|
|
)
|
|
}
|
|
|
|
/// Creates a vesting contract for a founder.
|
|
pub fn for_founder(
|
|
beneficiary: Address,
|
|
amount: u64,
|
|
start_time: u64,
|
|
) -> Result<Self, VestingError> {
|
|
Self::new(
|
|
beneficiary.clone(),
|
|
beneficiary, // Founder is their own grantor
|
|
VestingSchedule::founder(amount, start_time),
|
|
"Founder allocation".to_string(),
|
|
)
|
|
}
|
|
|
|
/// Returns the total vested amount at current time.
|
|
pub fn vested_amount(&self, timestamp: u64) -> u64 {
|
|
match self.state {
|
|
VestingState::Active => self.schedule.vested_at(timestamp),
|
|
VestingState::Revoked => {
|
|
// If revoked, vested amount is frozen at revocation time
|
|
if let Some(revoked_at) = self.revoked_at {
|
|
self.schedule.vested_at(revoked_at)
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
VestingState::Completed => self.schedule.total_amount,
|
|
}
|
|
}
|
|
|
|
/// Returns the claimable amount (vested - already claimed).
|
|
pub fn claimable_amount(&self, timestamp: u64) -> u64 {
|
|
let vested = self.vested_amount(timestamp);
|
|
vested.saturating_sub(self.claimed_amount)
|
|
}
|
|
|
|
/// Returns the unvested amount.
|
|
pub fn unvested_amount(&self, timestamp: u64) -> u64 {
|
|
self.schedule
|
|
.total_amount
|
|
.saturating_sub(self.vested_amount(timestamp))
|
|
}
|
|
|
|
/// Claims available tokens.
|
|
pub fn claim(&mut self, caller: &Address, timestamp: u64) -> Result<u64, VestingError> {
|
|
// Only beneficiary can claim
|
|
if caller != &self.beneficiary {
|
|
return Err(VestingError::Unauthorized);
|
|
}
|
|
|
|
// Check state
|
|
if self.state == VestingState::Completed {
|
|
return Err(VestingError::AlreadyCompleted);
|
|
}
|
|
|
|
// Calculate claimable
|
|
let claimable = self.claimable_amount(timestamp);
|
|
if claimable == 0 {
|
|
return Err(VestingError::NothingToClaim);
|
|
}
|
|
|
|
// Update claimed amount
|
|
self.claimed_amount += claimable;
|
|
|
|
// Check if completed
|
|
if self.claimed_amount >= self.schedule.total_amount {
|
|
self.state = VestingState::Completed;
|
|
}
|
|
|
|
Ok(claimable)
|
|
}
|
|
|
|
/// Revokes the vesting contract (only by grantor, if revocable).
|
|
pub fn revoke(&mut self, caller: &Address, timestamp: u64) -> Result<u64, VestingError> {
|
|
// Only grantor can revoke
|
|
if caller != &self.grantor {
|
|
return Err(VestingError::Unauthorized);
|
|
}
|
|
|
|
// Check if revocable
|
|
if !self.schedule.revocable {
|
|
return Err(VestingError::NotRevocable);
|
|
}
|
|
|
|
// Check state
|
|
if self.state == VestingState::Revoked {
|
|
return Err(VestingError::AlreadyRevoked);
|
|
}
|
|
if self.state == VestingState::Completed {
|
|
return Err(VestingError::AlreadyCompleted);
|
|
}
|
|
|
|
// Calculate unvested amount to return to grantor
|
|
let unvested = self.unvested_amount(timestamp);
|
|
|
|
// Update state
|
|
self.state = VestingState::Revoked;
|
|
self.revoked_at = Some(timestamp);
|
|
|
|
Ok(unvested)
|
|
}
|
|
|
|
/// Returns vesting progress as a percentage (0-100).
|
|
pub fn progress_percent(&self, timestamp: u64) -> f64 {
|
|
let vested = self.vested_amount(timestamp);
|
|
(vested as f64 / self.schedule.total_amount as f64) * 100.0
|
|
}
|
|
|
|
/// Returns summary information.
|
|
pub fn summary(&self, timestamp: u64) -> VestingSummary {
|
|
VestingSummary {
|
|
id: self.id,
|
|
beneficiary: self.beneficiary.clone(),
|
|
total: self.schedule.total_amount,
|
|
vested: self.vested_amount(timestamp),
|
|
claimed: self.claimed_amount,
|
|
claimable: self.claimable_amount(timestamp),
|
|
unvested: self.unvested_amount(timestamp),
|
|
state: self.state,
|
|
cliff_passed: self.schedule.cliff_passed(timestamp),
|
|
fully_vested: self.schedule.fully_vested(timestamp),
|
|
progress_percent: self.progress_percent(timestamp),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Summary of a vesting contract's current state.
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct VestingSummary {
|
|
pub id: Hash256,
|
|
pub beneficiary: Address,
|
|
pub total: u64,
|
|
pub vested: u64,
|
|
pub claimed: u64,
|
|
pub claimable: u64,
|
|
pub unvested: u64,
|
|
pub state: VestingState,
|
|
pub cliff_passed: bool,
|
|
pub fully_vested: bool,
|
|
pub progress_percent: f64,
|
|
}
|
|
|
|
/// Manages all vesting contracts.
|
|
pub struct VestingManager {
|
|
contracts: hashbrown::HashMap<Hash256, VestingContract>,
|
|
by_beneficiary: hashbrown::HashMap<Address, Vec<Hash256>>,
|
|
by_grantor: hashbrown::HashMap<Address, Vec<Hash256>>,
|
|
}
|
|
|
|
impl VestingManager {
|
|
/// Creates a new vesting manager.
|
|
pub fn new() -> Self {
|
|
VestingManager {
|
|
contracts: hashbrown::HashMap::new(),
|
|
by_beneficiary: hashbrown::HashMap::new(),
|
|
by_grantor: hashbrown::HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Creates a new vesting contract.
|
|
pub fn create_vesting(
|
|
&mut self,
|
|
beneficiary: Address,
|
|
grantor: Address,
|
|
schedule: VestingSchedule,
|
|
description: String,
|
|
) -> Result<Hash256, VestingError> {
|
|
let contract =
|
|
VestingContract::new(beneficiary.clone(), grantor.clone(), schedule, description)?;
|
|
let id = contract.id;
|
|
|
|
// Index by beneficiary
|
|
self.by_beneficiary.entry(beneficiary).or_default().push(id);
|
|
|
|
// Index by grantor
|
|
self.by_grantor.entry(grantor).or_default().push(id);
|
|
|
|
self.contracts.insert(id, contract);
|
|
Ok(id)
|
|
}
|
|
|
|
/// Gets a vesting contract by ID.
|
|
pub fn get(&self, id: &Hash256) -> Option<&VestingContract> {
|
|
self.contracts.get(id)
|
|
}
|
|
|
|
/// Gets mutable reference to a vesting contract.
|
|
pub fn get_mut(&mut self, id: &Hash256) -> Option<&mut VestingContract> {
|
|
self.contracts.get_mut(id)
|
|
}
|
|
|
|
/// Gets all vesting contracts for a beneficiary.
|
|
pub fn get_by_beneficiary(&self, beneficiary: &Address) -> Vec<&VestingContract> {
|
|
self.by_beneficiary
|
|
.get(beneficiary)
|
|
.map(|ids| ids.iter().filter_map(|id| self.contracts.get(id)).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Gets all vesting contracts created by a grantor.
|
|
pub fn get_by_grantor(&self, grantor: &Address) -> Vec<&VestingContract> {
|
|
self.by_grantor
|
|
.get(grantor)
|
|
.map(|ids| ids.iter().filter_map(|id| self.contracts.get(id)).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Claims tokens from a vesting contract.
|
|
pub fn claim(
|
|
&mut self,
|
|
id: &Hash256,
|
|
caller: &Address,
|
|
timestamp: u64,
|
|
) -> Result<u64, VestingError> {
|
|
let contract = self.contracts.get_mut(id).ok_or(VestingError::NotFound)?;
|
|
contract.claim(caller, timestamp)
|
|
}
|
|
|
|
/// Revokes a vesting contract.
|
|
pub fn revoke(
|
|
&mut self,
|
|
id: &Hash256,
|
|
caller: &Address,
|
|
timestamp: u64,
|
|
) -> Result<u64, VestingError> {
|
|
let contract = self.contracts.get_mut(id).ok_or(VestingError::NotFound)?;
|
|
contract.revoke(caller, timestamp)
|
|
}
|
|
|
|
/// Returns total vested amount across all contracts for a beneficiary.
|
|
pub fn total_vested_for(&self, beneficiary: &Address, timestamp: u64) -> u64 {
|
|
self.get_by_beneficiary(beneficiary)
|
|
.iter()
|
|
.map(|c| c.vested_amount(timestamp))
|
|
.sum()
|
|
}
|
|
|
|
/// Returns total claimable amount across all contracts for a beneficiary.
|
|
pub fn total_claimable_for(&self, beneficiary: &Address, timestamp: u64) -> u64 {
|
|
self.get_by_beneficiary(beneficiary)
|
|
.iter()
|
|
.map(|c| c.claimable_amount(timestamp))
|
|
.sum()
|
|
}
|
|
|
|
/// Returns all active contracts.
|
|
pub fn active_contracts(&self) -> Vec<&VestingContract> {
|
|
self.contracts
|
|
.values()
|
|
.filter(|c| c.state == VestingState::Active)
|
|
.collect()
|
|
}
|
|
|
|
/// Returns statistics.
|
|
pub fn stats(&self) -> VestingStats {
|
|
let contracts: Vec<_> = self.contracts.values().collect();
|
|
VestingStats {
|
|
total_contracts: contracts.len(),
|
|
active_contracts: contracts
|
|
.iter()
|
|
.filter(|c| c.state == VestingState::Active)
|
|
.count(),
|
|
revoked_contracts: contracts
|
|
.iter()
|
|
.filter(|c| c.state == VestingState::Revoked)
|
|
.count(),
|
|
completed_contracts: contracts
|
|
.iter()
|
|
.filter(|c| c.state == VestingState::Completed)
|
|
.count(),
|
|
total_amount: contracts.iter().map(|c| c.schedule.total_amount).sum(),
|
|
total_claimed: contracts.iter().map(|c| c.claimed_amount).sum(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for VestingManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Vesting statistics.
|
|
#[derive(Clone, Debug)]
|
|
pub struct VestingStats {
|
|
pub total_contracts: usize,
|
|
pub active_contracts: usize,
|
|
pub revoked_contracts: usize,
|
|
pub completed_contracts: usize,
|
|
pub total_amount: u64,
|
|
pub total_claimed: u64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn test_address(n: u8) -> Address {
|
|
let mut bytes = [0u8; 32];
|
|
bytes[0] = n;
|
|
Address::from_parts(
|
|
synor_types::Network::Devnet,
|
|
synor_types::address::AddressType::P2PKH,
|
|
bytes,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_vesting_schedule_cliff() {
|
|
let schedule = VestingSchedule::standard_4year(1_000_000, 0);
|
|
|
|
// Implementation uses 30-day months, so 12 months = 360 days
|
|
let seconds_per_month: u64 = 30 * 24 * 60 * 60;
|
|
let cliff_12_months = 12 * seconds_per_month; // 31104000
|
|
let vesting_48_months = 48 * seconds_per_month; // 124416000
|
|
|
|
// Before cliff
|
|
assert_eq!(schedule.vested_at(0), 0);
|
|
assert_eq!(schedule.vested_at(6 * seconds_per_month), 0); // 6 months
|
|
assert_eq!(schedule.vested_at(cliff_12_months - 1), 0); // Just before cliff
|
|
|
|
// At cliff - some tokens should be vested (linear from 0)
|
|
assert!(schedule.vested_at(cliff_12_months) > 0);
|
|
|
|
// After full vesting (48 months)
|
|
assert_eq!(schedule.vested_at(vesting_48_months), 1_000_000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vesting_contract_claim() {
|
|
let beneficiary = test_address(1);
|
|
let grantor = test_address(2);
|
|
let start_time = 0;
|
|
|
|
let mut contract = VestingContract::new(
|
|
beneficiary.clone(),
|
|
grantor,
|
|
VestingSchedule::new(1_000_000, 0, 12, start_time, true), // 1 year, no cliff
|
|
"Test vesting".to_string(),
|
|
)
|
|
.unwrap();
|
|
|
|
// After 6 months, should have ~50% vested
|
|
let six_months = 6 * 30 * 24 * 60 * 60;
|
|
let claimable = contract.claimable_amount(six_months);
|
|
assert!(claimable > 400_000 && claimable < 600_000);
|
|
|
|
// Claim
|
|
let claimed = contract.claim(&beneficiary, six_months).unwrap();
|
|
assert_eq!(claimed, claimable);
|
|
assert_eq!(contract.claimed_amount, claimable);
|
|
|
|
// No more claimable immediately
|
|
assert_eq!(contract.claimable_amount(six_months), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vesting_revocation() {
|
|
let beneficiary = test_address(1);
|
|
let grantor = test_address(2);
|
|
|
|
let mut contract = VestingContract::new(
|
|
beneficiary,
|
|
grantor.clone(),
|
|
VestingSchedule::new(1_000_000, 0, 12, 0, true),
|
|
"Revocable".to_string(),
|
|
)
|
|
.unwrap();
|
|
|
|
// Revoke at 6 months
|
|
let six_months = 6 * 30 * 24 * 60 * 60;
|
|
let unvested = contract.revoke(&grantor, six_months).unwrap();
|
|
|
|
assert!(unvested > 400_000); // ~50% unvested returned
|
|
assert_eq!(contract.state, VestingState::Revoked);
|
|
}
|
|
|
|
#[test]
|
|
fn test_non_revocable() {
|
|
let beneficiary = test_address(1);
|
|
let grantor = test_address(2);
|
|
|
|
let mut contract = VestingContract::new(
|
|
beneficiary,
|
|
grantor.clone(),
|
|
VestingSchedule::new(1_000_000, 0, 12, 0, false), // non-revocable
|
|
"Non-revocable".to_string(),
|
|
)
|
|
.unwrap();
|
|
|
|
let result = contract.revoke(&grantor, 0);
|
|
assert!(matches!(result, Err(VestingError::NotRevocable)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_vesting_manager() {
|
|
let mut manager = VestingManager::new();
|
|
let beneficiary = test_address(1);
|
|
let grantor = test_address(2);
|
|
|
|
let id = manager
|
|
.create_vesting(
|
|
beneficiary.clone(),
|
|
grantor,
|
|
VestingSchedule::new(1_000_000, 0, 12, 0, true),
|
|
"Test".to_string(),
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(manager.get(&id).is_some());
|
|
assert_eq!(manager.get_by_beneficiary(&beneficiary).len(), 1);
|
|
|
|
let stats = manager.stats();
|
|
assert_eq!(stats.total_contracts, 1);
|
|
assert_eq!(stats.active_contracts, 1);
|
|
}
|
|
}
|