diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 64c44ef..22e6d56 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -25,9 +25,26 @@ jobs:
- name: Install Stellar CLI
run: |
- wget https://github.com/stellar/stellar-cli/releases/download/v25.1.0/stellar-cli-25.1.0-x86_64-unknown-linux-gnu.tar.gz
- tar -xzf stellar-cli-25.1.0-x86_64-unknown-linux-gnu.tar.gz
- sudo mv stellar-cli-25.1.0-x86_64-unknown-linux-gnu/stellar /usr/local/bin/
+ set -euo pipefail
+
+ VERSION="25.1.0"
+ ARCHIVE="stellar-cli-${VERSION}-x86_64-unknown-linux-gnu.tar.gz"
+ URL="https://github.com/stellar/stellar-cli/releases/download/v${VERSION}/${ARCHIVE}"
+
+ wget "${URL}" -O "${ARCHIVE}"
+
+ # Package layout has changed across versions; locate the binary from the archive contents.
+ BIN_PATH="$(tar -tzf "${ARCHIVE}" | grep -E '(^|/)stellar(-cli)?$' | head -n 1 || true)"
+ if [ -z "${BIN_PATH}" ]; then
+ echo "Could not locate stellar CLI binary in ${ARCHIVE}"
+ tar -tzf "${ARCHIVE}"
+ exit 1
+ fi
+
+ tar -xzf "${ARCHIVE}"
+
+ sudo install -m 0755 "${BIN_PATH}" /usr/local/bin/stellar
+ stellar --version
- name: Build Contract
run: cargo build --target wasm32-unknown-unknown --release
diff --git a/contracts/vesting_contracts/src/factory.rs b/contracts/vesting_contracts/src/factory.rs
index 93c1cd7..5710062 100644
--- a/contracts/vesting_contracts/src/factory.rs
+++ b/contracts/vesting_contracts/src/factory.rs
@@ -1,4 +1,6 @@
-use soroban_sdk::{contract, contractimpl, contractmeta, contracttype, Map, Address, BytesN, Env, Vec};
+use soroban_sdk::{
+ contract, contractimpl, contractmeta, contracttype, Address, BytesN, Env, Map, Vec,
+};
// Contract metadata for the factory
contractmeta!(
@@ -18,7 +20,7 @@ enum DataKey {
#[contractimpl]
impl VestingFactory {
/// Initialize the factory with the WASM hash of the vesting contract
- pub fn initialize(env: Env, wasm_hash: BytesN<32>) {
+ pub fn initialize_factory(env: Env, wasm_hash: BytesN<32>) {
// Store the WASM hash for future deployments
env.storage().instance().set(&DataKey::WasmHash, &wasm_hash);
@@ -31,7 +33,12 @@ impl VestingFactory {
/// Deploy a new vesting contract for an organization
/// Only allows deployment if token is whitelisted
- pub fn deploy_new_vault_contract(env: Env, admin: Address, initial_supply: i128, token: Address) -> Address {
+ pub fn deploy_new_vault_contract(
+ env: Env,
+ admin: Address,
+ initial_supply: i128,
+ token: Address,
+ ) -> Address {
let _wasm_hash: BytesN<32> = env
.storage()
.instance()
@@ -39,7 +46,11 @@ impl VestingFactory {
.unwrap_or_else(|| panic!("Factory not initialized - WASM hash not set"));
// Check token whitelist
- let whitelist: Map
= env.storage().instance().get(&crate::WhitelistDataKey::WhitelistedTokens).unwrap_or(Map::new(&env));
+ let whitelist: Map = env
+ .storage()
+ .instance()
+ .get(&crate::WhitelistDataKey::WhitelistedTokens)
+ .unwrap_or(Map::new(&env));
if !whitelist.get(token.clone()).unwrap_or(false) {
panic!("Token not whitelisted");
}
diff --git a/contracts/vesting_contracts/src/lib.rs b/contracts/vesting_contracts/src/lib.rs
index 726dad6..dfb2438 100644
--- a/contracts/vesting_contracts/src/lib.rs
+++ b/contracts/vesting_contracts/src/lib.rs
@@ -1,11 +1,9 @@
#![no_std]
-use soroban_sdk::{contract, contractimpl, contracttype, token, vec, Address, Env, IntoVal, Map, Symbol, Vec, String};
+
use soroban_sdk::{
- contract, contractimpl, contracttype, token, vec, Address, Env, IntoVal, Map, Symbol, Vec,
- String,
+ contract, contractimpl, contracttype, token, vec, Address, Env, IntoVal, Map, String, Symbol,
+ Vec,
};
- #![no_std]
-use soroban_sdk::{contract, contractimpl, contracttype, token, vec, Address, Env, IntoVal, Map, Symbol, Vec};
// DataKey for whitelisted tokens
#[contracttype]
@@ -27,40 +25,17 @@ pub enum DataKey {
IsPaused,
IsDeprecated,
MigrationTarget,
-}
-
-mod factory;
-pub use factory::{VestingFactory, VestingFactoryClient};
-
-#[contract]
-pub struct VestingContract;
-
-// Vault structure with lazy initialization
-#[contracttype]
-pub enum DataKey {
- AdminAddress,
- AdminBalance,
- InitialSupply,
- ProposedAdmin,
- VaultCount,
- VaultData(u64),
- VaultMilestones(u64),
- UserVaults(Address),
- IsPaused,
- KeeperFees,
- Token, // yield-bearing token
- TotalShares, // remaining initial_deposit_shares
+ Token, // yield-bearing token
+ TotalShares, // remaining initial_deposit_shares
TotalStaked,
}
#[contracttype]
#[derive(Clone)]
-// NOTE: `#[contracttype]` structs serialize by field order (tuple-style).
-// Reordering fields is a storage schema change; only do this pre-deploy or with migration.
pub struct Vault {
pub total_amount: i128, // = initial_deposit_shares
pub released_amount: i128,
- pub keeper_fee: i128, // Fee paid to anyone who triggers auto_claim
+ pub keeper_fee: i128, // Fee paid to anyone who triggers auto_claim
pub staked_amount: i128, // Amount currently staked in external contract
pub owner: Address,
@@ -72,19 +47,10 @@ pub struct Vault {
pub creation_time: u64, // Timestamp of creation for clawback grace period
pub step_duration: u64, // Duration of each vesting step in seconds (0 = linear)
- pub is_initialized: bool, // Lazy initialization flag
- pub is_irrevocable: bool, // Security flag to prevent admin withdrawal
+ pub is_initialized: bool, // Lazy initialization flag
+ pub is_irrevocable: bool, // Security flag to prevent admin withdrawal
pub is_transferable: bool, // Can the beneficiary transfer this vault?
- pub step_duration: u64, // Duration of each vesting step in seconds (0 = linear)
- pub staked_amount: i128, // Amount currently staked in external contract
- pub is_frozen: bool, // Individual vault freeze flag for security investigations
- pub keeper_fee: i128,
- pub is_initialized: bool,
- pub is_irrevocable: bool,
- pub creation_time: u64,
- pub is_transferable: bool,
- pub step_duration: u64,
- pub staked_amount: i128,
+ pub is_frozen: bool, // Individual vault freeze flag for security investigations
}
#[contracttype]
@@ -181,34 +147,29 @@ impl VestingContract {
pub fn initialize(env: Env, admin: Address, initial_supply: i128) {
Self::require_not_deprecated(&env);
+ env.storage().instance().set(&DataKey::AdminAddress, &admin);
env.storage()
.instance()
.set(&DataKey::InitialSupply, &initial_supply);
-
env.storage()
.instance()
.set(&DataKey::AdminBalance, &initial_supply);
-
- pub fn initialize(env: Env, admin: Address, initial_supply: i128) {
- env.storage().instance().set(&DataKey::InitialSupply, &initial_supply);
- env.storage().instance().set(&DataKey::AdminBalance, &initial_supply);
- env.storage().instance().set(&DataKey::AdminAddress, &admin);
env.storage().instance().set(&DataKey::VaultCount, &0u64);
// Initialize pause state to false (unpaused)
env.storage().instance().set(&DataKey::IsPaused, &false);
// Initialize deprecated state to false (active)
- env.storage()
- .instance()
- .set(&DataKey::IsDeprecated, &false);
+ env.storage().instance().set(&DataKey::IsDeprecated, &false);
// Clear migration target on init
env.storage().instance().remove(&DataKey::MigrationTarget);
// Initialize whitelisted tokens map
let whitelist: Map = Map::new(&env);
- env.storage().instance().set(&WhitelistDataKey::WhitelistedTokens, &whitelist);
+ env.storage()
+ .instance()
+ .set(&WhitelistDataKey::WhitelistedTokens, &whitelist);
env.storage().instance().set(&DataKey::TotalShares, &0i128);
env.storage().instance().set(&DataKey::TotalStaked, &0i128);
@@ -260,7 +221,11 @@ impl VestingContract {
pct = pct.saturating_add(m.percentage);
}
}
- if pct > 100 { 100 } else { pct }
+ if pct > 100 {
+ 100
+ } else {
+ pct
+ }
}
fn unlocked_amount(total_amount: i128, unlocked_percentage: u32) -> i128 {
@@ -269,7 +234,9 @@ impl VestingContract {
pub fn propose_new_admin(env: Env, new_admin: Address) {
Self::require_admin(&env);
- env.storage().instance().set(&DataKey::ProposedAdmin, &new_admin);
+ env.storage()
+ .instance()
+ .set(&DataKey::ProposedAdmin, &new_admin);
}
pub fn accept_ownership(env: Env) {
@@ -280,7 +247,9 @@ impl VestingContract {
.get(&DataKey::ProposedAdmin)
.unwrap_or_else(|| panic!("No proposed admin found"));
proposed_admin.require_auth();
- env.storage().instance().set(&DataKey::AdminAddress, &proposed_admin);
+ env.storage()
+ .instance()
+ .set(&DataKey::AdminAddress, &proposed_admin);
env.storage().instance().remove(&DataKey::ProposedAdmin);
}
@@ -337,15 +306,20 @@ impl VestingContract {
);
}
- env.events()
- .publish(Symbol::new(&env, "ContractDeprecated"), v2_contract_address);
+ env.events().publish(
+ (Symbol::new(&env, "ContractDeprecated"),),
+ v2_contract_address,
+ );
migrated
}
// Get current admin address
pub fn get_admin(env: Env) -> Address {
- env.storage().instance().get(&DataKey::AdminAddress).unwrap_or_else(|| panic!("Admin not set"))
+ env.storage()
+ .instance()
+ .get(&DataKey::AdminAddress)
+ .unwrap_or_else(|| panic!("Admin not set"))
}
pub fn get_proposed_admin(env: Env) -> Option {
@@ -355,15 +329,18 @@ impl VestingContract {
// Toggle pause state (Admin only) - "Big Red Button" for emergency pause
pub fn toggle_pause(env: Env) {
Self::require_admin(&env);
-
- let current_pause_state: bool = env.storage()
+
+ let current_pause_state: bool = env
+ .storage()
.instance()
.get(&DataKey::IsPaused)
.unwrap_or(false);
-
+
let new_pause_state = !current_pause_state;
- env.storage().instance().set(&DataKey::IsPaused, &new_pause_state);
-
+ env.storage()
+ .instance()
+ .set(&DataKey::IsPaused, &new_pause_state);
+
// Emit event for pause state change
env.events().publish(
(Symbol::new(&env, "PauseToggled"),),
@@ -440,7 +417,6 @@ impl VestingContract {
vault.is_frozen
}
-
// Full initialization - writes all metadata immediately
pub fn create_vault_full(
env: Env,
@@ -455,17 +431,30 @@ impl VestingContract {
) -> u64 {
Self::require_admin(&env);
- let mut vault_count: u64 = env.storage().instance().get(&DataKey::VaultCount).unwrap_or(0);
+ let mut vault_count: u64 = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultCount)
+ .unwrap_or(0);
vault_count += 1;
- let mut admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
- if admin_balance < amount { panic!("Insufficient admin balance"); }
+ let mut admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
+ if admin_balance < amount {
+ panic!("Insufficient admin balance");
+ }
admin_balance -= amount;
- env.storage().instance().set(&DataKey::AdminBalance, &admin_balance);
+ env.storage()
+ .instance()
+ .set(&DataKey::AdminBalance, &admin_balance);
let now = env.ledger().timestamp();
let vault = Vault {
+ title: String::from_slice(&env, ""),
owner: owner.clone(),
delegate: None,
total_amount: amount,
@@ -482,21 +471,46 @@ impl VestingContract {
is_frozen: false,
};
- env.storage().instance().set(&DataKey::VaultData(vault_count), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_count), &vault);
- let mut user_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(owner.clone())).unwrap_or(Vec::new(&env));
+ let mut user_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(owner.clone()))
+ .unwrap_or(Vec::new(&env));
user_vaults.push_back(vault_count);
- env.storage().instance().set(&DataKey::UserVaults(owner.clone()), &user_vaults);
+ env.storage()
+ .instance()
+ .set(&DataKey::UserVaults(owner.clone()), &user_vaults);
- env.storage().instance().set(&DataKey::VaultCount, &vault_count);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultCount, &vault_count);
- let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
+ let mut total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
total_shares += amount;
- env.storage().instance().set(&DataKey::TotalShares, &total_shares);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &total_shares);
let cliff_duration = start_time.saturating_sub(now);
- let vault_created = VaultCreated { vault_id: vault_count, beneficiary: owner, total_amount: amount, cliff_duration, start_time };
- env.events().publish((Symbol::new(&env, "VaultCreated"), vault_count), vault_created);
+ let vault_created = VaultCreated {
+ vault_id: vault_count,
+ beneficiary: owner,
+ total_amount: amount,
+ cliff_duration,
+ start_time,
+ };
+ env.events().publish(
+ (Symbol::new(&env, "VaultCreated"), vault_count),
+ vault_created,
+ );
vault_count
}
@@ -514,17 +528,30 @@ impl VestingContract {
) -> u64 {
Self::require_admin(&env);
- let mut vault_count: u64 = env.storage().instance().get(&DataKey::VaultCount).unwrap_or(0);
+ let mut vault_count: u64 = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultCount)
+ .unwrap_or(0);
vault_count += 1;
- let mut admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
- if admin_balance < amount { panic!("Insufficient admin balance"); }
+ let mut admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
+ if admin_balance < amount {
+ panic!("Insufficient admin balance");
+ }
admin_balance -= amount;
- env.storage().instance().set(&DataKey::AdminBalance, &admin_balance);
+ env.storage()
+ .instance()
+ .set(&DataKey::AdminBalance, &admin_balance);
let now = env.ledger().timestamp();
let vault = Vault {
+ title: String::from_slice(&env, ""),
owner: owner.clone(),
delegate: None,
total_amount: amount,
@@ -541,22 +568,46 @@ impl VestingContract {
is_frozen: false,
};
- env.storage().instance().set(&DataKey::VaultData(vault_count), &vault);
- env.storage().instance().set(&DataKey::VaultCount, &vault_count);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_count), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultCount, &vault_count);
- let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
+ let mut total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
total_shares += amount;
- env.storage().instance().set(&DataKey::TotalShares, &total_shares);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &total_shares);
let cliff_duration = start_time.saturating_sub(now);
- let vault_created = VaultCreated { vault_id: vault_count, beneficiary: owner.clone(), total_amount: amount, cliff_duration, start_time };
- env.events().publish((Symbol::new(&env, "VaultCreated"), vault_count), vault_created);
+ let vault_created = VaultCreated {
+ vault_id: vault_count,
+ beneficiary: owner.clone(),
+ total_amount: amount,
+ cliff_duration,
+ start_time,
+ };
+ env.events().publish(
+ (Symbol::new(&env, "VaultCreated"), vault_count),
+ vault_created,
+ );
vault_count
}
fn initialize_vault_metadata(env: &Env, vault_id: u64) -> bool {
- if env.storage().instance().get(&DataKey::IsDeprecated).unwrap_or(false) {
+ if env
+ .storage()
+ .instance()
+ .get(&DataKey::IsDeprecated)
+ .unwrap_or(false)
+ {
return false;
}
@@ -565,17 +616,24 @@ impl VestingContract {
.instance()
.get(&DataKey::VaultData(vault_id))
.unwrap_or_else(|| panic!("Vault not found"));
- let vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
if !vault.is_initialized {
let mut updated_vault = vault.clone();
updated_vault.is_initialized = true;
- env.storage().instance().set(&DataKey::VaultData(vault_id), &updated_vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &updated_vault);
- let mut user_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(updated_vault.owner.clone())).unwrap_or(Vec::new(env));
+ let mut user_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(updated_vault.owner.clone()))
+ .unwrap_or(Vec::new(env));
user_vaults.push_back(vault_id);
- env.storage().instance().set(&DataKey::UserVaults(updated_vault.owner), &user_vaults);
+ env.storage()
+ .instance()
+ .set(&DataKey::UserVaults(updated_vault.owner), &user_vaults);
true
} else {
@@ -585,35 +643,25 @@ impl VestingContract {
fn calculate_time_vested_amount(env: &Env, vault: &Vault) -> i128 {
let now = env.ledger().timestamp();
- if now < vault.start_time { return 0; }
- if now >= vault.end_time { return vault.total_amount; }
+ if now <= vault.start_time {
+ return 0;
+ }
+ if now >= vault.end_time {
+ return vault.total_amount;
+ }
+
let duration = vault.end_time - vault.start_time;
- if duration == 0 { return vault.total_amount; }
+ if duration == 0 {
+ return vault.total_amount;
+ }
+
let elapsed = now - vault.start_time;
- let _effective_elapsed = if vault.step_duration > 0 {
+ let effective_elapsed = if vault.step_duration > 0 {
(elapsed / vault.step_duration) * vault.step_duration
} else {
elapsed
};
- if vault.step_duration > 0 {
- // Periodic vesting: calculate vested = (elapsed / step_duration) * rate * step_duration
- // Rate is total_amount / duration, so: vested = (elapsed / step_duration) * (total_amount / duration) * step_duration
- // This simplifies to: vested = (elapsed / step_duration) * total_amount * step_duration / duration
- let completed_steps = elapsed / vault.step_duration;
- let rate_per_second = vault.total_amount / duration as i128;
- let vested = completed_steps as i128 * rate_per_second * vault.step_duration as i128;
-
- // Ensure we don't exceed total amount
- if vested > vault.total_amount {
- vault.total_amount
- } else {
- vested
- }
- } else {
- // Linear vesting
- (vault.total_amount * elapsed as i128) / duration as i128
- }
(vault.total_amount * effective_elapsed as i128) / duration as i128
}
@@ -635,12 +683,14 @@ impl VestingContract {
if claim_amount <= 0 {
panic!("Claim amount must be positive");
}
- if !vault.is_initialized { panic!("Vault not initialized"); }
- if claim_amount <= 0 { panic!("Claim amount must be positive"); }
vault.owner.require_auth();
- let unlocked_amount = if env.storage().instance().has(&DataKey::VaultMilestones(vault_id)) {
+ let unlocked_amount = if env
+ .storage()
+ .instance()
+ .has(&DataKey::VaultMilestones(vault_id))
+ {
let milestones = Self::require_milestones_configured(&env, vault_id);
let unlocked_pct = Self::unlocked_percentage(&milestones);
Self::unlocked_amount(vault.total_amount, unlocked_pct)
@@ -663,22 +713,44 @@ impl VestingContract {
vault.staked_amount -= deficit;
- let mut total_staked: i128 = env.storage().instance().get(&DataKey::TotalStaked).unwrap_or(0);
+ let mut total_staked: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalStaked)
+ .unwrap_or(0);
total_staked -= deficit;
- env.storage().instance().set(&DataKey::TotalStaked, &total_staked);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalStaked, &total_staked);
}
let available_to_claim = unlocked_amount - vault.released_amount;
- if available_to_claim <= 0 { panic!("No tokens available to claim"); }
- if claim_amount > available_to_claim { panic!("Insufficient unlocked tokens to claim"); }
+ if available_to_claim <= 0 {
+ panic!("No tokens available to claim");
+ }
+ if claim_amount > available_to_claim {
+ panic!("Insufficient unlocked tokens to claim");
+ }
// YIELD DISTRIBUTION - only vault-owned portion
let token_client = Self::get_token_client(&env);
let current_balance = token_client.balance(&env.current_contract_address());
- let admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
+ let admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
- let total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
- let total_staked: i128 = env.storage().instance().get(&DataKey::TotalStaked).unwrap_or(0);
+ let total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
+ let total_staked: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalStaked)
+ .unwrap_or(0);
let liquid_shares = total_shares - total_staked;
let vault_portion = (current_balance - admin_balance).max(0);
@@ -691,10 +763,18 @@ impl VestingContract {
vault.released_amount += claim_amount;
let mut updated_total_shares = total_shares;
updated_total_shares -= claim_amount;
- env.storage().instance().set(&DataKey::TotalShares, &updated_total_shares);
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &updated_total_shares);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- token_client.transfer(&env.current_contract_address(), &vault.owner, &transfer_amount);
+ token_client.transfer(
+ &env.current_contract_address(),
+ &vault.owner,
+ &transfer_amount,
+ );
transfer_amount
}
@@ -702,29 +782,50 @@ impl VestingContract {
pub fn transfer_beneficiary(env: Env, vault_id: u64, new_address: Address) {
Self::require_admin(&env);
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let mut vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
let old_owner = vault.owner.clone();
if vault.is_initialized {
- let old_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(old_owner.clone())).unwrap_or(Vec::new(&env));
+ let old_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(old_owner.clone()))
+ .unwrap_or(Vec::new(&env));
let mut updated_old_vaults = Vec::new(&env);
for id in old_vaults.iter() {
if id != vault_id {
updated_old_vaults.push_back(id);
}
}
- env.storage().instance().set(&DataKey::UserVaults(old_owner.clone()), &updated_old_vaults);
+ env.storage()
+ .instance()
+ .set(&DataKey::UserVaults(old_owner.clone()), &updated_old_vaults);
- let mut new_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(new_address.clone())).unwrap_or(Vec::new(&env));
+ let mut new_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(new_address.clone()))
+ .unwrap_or(Vec::new(&env));
new_vaults.push_back(vault_id);
- env.storage().instance().set(&DataKey::UserVaults(new_address.clone()), &new_vaults);
+ env.storage()
+ .instance()
+ .set(&DataKey::UserVaults(new_address.clone()), &new_vaults);
}
vault.owner = new_address.clone();
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- env.events().publish((Symbol::new(&env, "BeneficiaryUpdated"), vault_id), (old_owner.clone(), new_address));
+ env.events().publish(
+ (Symbol::new(&env, "BeneficiaryUpdated"), vault_id),
+ (old_owner.clone(), new_address),
+ );
}
pub fn set_delegate(env: Env, vault_id: u64, delegate: Option) {
@@ -734,23 +835,27 @@ impl VestingContract {
.instance()
.get(&DataKey::VaultData(vault_id))
.unwrap_or_else(|| panic!("Vault not found"));
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
- if !vault.is_initialized { panic!("Vault not initialized"); }
+ if !vault.is_initialized {
+ panic!("Vault not initialized");
+ }
vault.owner.require_auth();
let old_delegate = vault.delegate.clone();
vault.delegate = delegate.clone();
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- env.events().publish((Symbol::new(&env, "DelegateUpdated"), vault_id), (old_delegate, delegate));
+ env.events().publish(
+ (Symbol::new(&env, "DelegateUpdated"), vault_id),
+ (old_delegate, delegate),
+ );
}
pub fn claim_as_delegate(env: Env, vault_id: u64, claim_amount: i128) -> i128 {
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
-
let vault: Vault = env
.storage()
.instance()
@@ -768,26 +873,51 @@ impl VestingContract {
if claim_amount <= 0 {
panic!("Claim amount must be positive");
}
- if !vault.is_initialized { panic!("Vault not initialized"); }
- if claim_amount <= 0 { panic!("Claim amount must be positive"); }
- let delegate = vault.delegate.clone().unwrap_or_else(|| panic!("No delegate set for this vault"));
+ let delegate = vault
+ .delegate
+ .clone()
+ .unwrap_or_else(|| panic!("No delegate set for this vault"));
delegate.require_auth();
- let milestones = Self::require_milestones_configured(&env, vault_id);
- let unlocked_pct = Self::unlocked_percentage(&milestones);
- let unlocked_amount = Self::unlocked_amount(vault.total_amount, unlocked_pct);
+ let unlocked_amount = if env
+ .storage()
+ .instance()
+ .has(&DataKey::VaultMilestones(vault_id))
+ {
+ let milestones = Self::require_milestones_configured(&env, vault_id);
+ let unlocked_pct = Self::unlocked_percentage(&milestones);
+ Self::unlocked_amount(vault.total_amount, unlocked_pct)
+ } else {
+ Self::calculate_time_vested_amount(&env, &vault)
+ };
let available_to_claim = unlocked_amount - vault.released_amount;
- if available_to_claim <= 0 { panic!("No tokens available to claim"); }
- if claim_amount > available_to_claim { panic!("Insufficient unlocked tokens to claim"); }
+ if available_to_claim <= 0 {
+ panic!("No tokens available to claim");
+ }
+ if claim_amount > available_to_claim {
+ panic!("Insufficient unlocked tokens to claim");
+ }
// YIELD DISTRIBUTION - only vault-owned portion
let token_client = Self::get_token_client(&env);
let current_balance = token_client.balance(&env.current_contract_address());
- let admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
+ let admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
- let total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
- let total_staked: i128 = env.storage().instance().get(&DataKey::TotalStaked).unwrap_or(0);
+ let total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
+ let total_staked: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalStaked)
+ .unwrap_or(0);
let liquid_shares = total_shares - total_staked;
let vault_portion = (current_balance - admin_balance).max(0);
@@ -802,10 +932,18 @@ impl VestingContract {
let mut updated_total_shares = total_shares;
updated_total_shares -= claim_amount;
- env.storage().instance().set(&DataKey::TotalShares, &updated_total_shares);
- env.storage().instance().set(&DataKey::VaultData(vault_id), &updated_vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &updated_total_shares);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &updated_vault);
- token_client.transfer(&env.current_contract_address(), &updated_vault.owner, &transfer_amount);
+ token_client.transfer(
+ &env.current_contract_address(),
+ &updated_vault.owner,
+ &transfer_amount,
+ );
transfer_amount
}
@@ -813,34 +951,62 @@ impl VestingContract {
pub fn set_milestones(env: Env, vault_id: u64, milestones: Vec) {
Self::require_admin(&env);
- let vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
- if !vault.is_initialized { panic!("Vault not initialized"); }
+ let vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
+ if !vault.is_initialized {
+ panic!("Vault not initialized");
+ }
- if milestones.is_empty() { panic!("No milestones provided"); }
+ if milestones.is_empty() {
+ panic!("No milestones provided");
+ }
let mut total_pct: u32 = 0;
let mut seen: Map = Map::new(&env);
for m in milestones.iter() {
- if m.percentage == 0 { panic!("Milestone percentage must be positive"); }
- if m.percentage > 100 { panic!("Milestone percentage too large"); }
- if seen.contains_key(m.id) { panic!("Duplicate milestone id"); }
+ if m.percentage == 0 {
+ panic!("Milestone percentage must be positive");
+ }
+ if m.percentage > 100 {
+ panic!("Milestone percentage too large");
+ }
+ if seen.contains_key(m.id) {
+ panic!("Duplicate milestone id");
+ }
seen.set(m.id, true);
total_pct = total_pct.saturating_add(m.percentage);
}
- if total_pct > 100 { panic!("Total milestone percentage exceeds 100"); }
+ if total_pct > 100 {
+ panic!("Total milestone percentage exceeds 100");
+ }
- env.storage().instance().set(&DataKey::VaultMilestones(vault_id), &milestones);
- env.events().publish((Symbol::new(&env, "MilestonesSet"), vault_id), (milestones.len(), total_pct));
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultMilestones(vault_id), &milestones);
+ env.events().publish(
+ (Symbol::new(&env, "MilestonesSet"), vault_id),
+ (milestones.len(), total_pct),
+ );
}
pub fn get_milestones(env: Env, vault_id: u64) -> Vec {
- env.storage().instance().get(&DataKey::VaultMilestones(vault_id)).unwrap_or(Vec::new(&env))
+ env.storage()
+ .instance()
+ .get(&DataKey::VaultMilestones(vault_id))
+ .unwrap_or(Vec::new(&env))
}
pub fn unlock_milestone(env: Env, vault_id: u64, milestone_id: u64) {
Self::require_admin(&env);
- let _vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let _vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
let milestones = Self::require_milestones_configured(&env, vault_id);
@@ -849,36 +1015,62 @@ impl VestingContract {
for m in milestones.iter() {
if m.id == milestone_id {
found = true;
- if m.is_unlocked { panic!("Milestone already unlocked"); }
- updated.push_back(Milestone { id: m.id, percentage: m.percentage, is_unlocked: true });
+ if m.is_unlocked {
+ panic!("Milestone already unlocked");
+ }
+ updated.push_back(Milestone {
+ id: m.id,
+ percentage: m.percentage,
+ is_unlocked: true,
+ });
} else {
updated.push_back(m);
}
}
- if !found { panic!("Milestone not found"); }
+ if !found {
+ panic!("Milestone not found");
+ }
- env.storage().instance().set(&DataKey::VaultMilestones(vault_id), &updated);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultMilestones(vault_id), &updated);
let timestamp = env.ledger().timestamp();
- env.events().publish((Symbol::new(&env, "MilestoneUnlocked"), vault_id), (milestone_id, timestamp));
+ env.events().publish(
+ (Symbol::new(&env, "MilestoneUnlocked"), vault_id),
+ (milestone_id, timestamp),
+ );
}
pub fn batch_create_vaults_lazy(env: Env, batch_data: BatchCreateData) -> Vec {
Self::require_admin(&env);
let mut vault_ids = Vec::new(&env);
- let initial_count: u64 = env.storage().instance().get(&DataKey::VaultCount).unwrap_or(0);
+ let initial_count: u64 = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultCount)
+ .unwrap_or(0);
let total_amount: i128 = batch_data.amounts.iter().sum();
- let mut admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
- if admin_balance < total_amount { panic!("Insufficient admin balance for batch"); }
+ let mut admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
+ if admin_balance < total_amount {
+ panic!("Insufficient admin balance for batch");
+ }
admin_balance -= total_amount;
- env.storage().instance().set(&DataKey::AdminBalance, &admin_balance);
+ env.storage()
+ .instance()
+ .set(&DataKey::AdminBalance, &admin_balance);
let now = env.ledger().timestamp();
for i in 0..batch_data.recipients.len() {
let vault_id = initial_count + i as u64 + 1;
let vault = Vault {
+ title: String::from_slice(&env, ""),
owner: batch_data.recipients.get(i).unwrap(),
delegate: None,
total_amount: batch_data.amounts.get(i).unwrap(),
@@ -886,9 +1078,6 @@ impl VestingContract {
start_time: batch_data.start_times.get(i).unwrap(),
end_time: batch_data.end_times.get(i).unwrap(),
keeper_fee: batch_data.keeper_fees.get(i).unwrap(),
- title: String::from_slice(&env, ""),
- is_initialized: false, // Lazy initialization
- is_irrevocable: false, // Default to revocable for batch operations
is_initialized: false,
is_irrevocable: false,
creation_time: now,
@@ -898,21 +1087,38 @@ impl VestingContract {
is_frozen: false,
};
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
vault_ids.push_back(vault_id);
let start_time = batch_data.start_times.get(i).unwrap();
let cliff_duration = start_time.saturating_sub(now);
- let vault_created = VaultCreated { vault_id, beneficiary: vault.owner.clone(), total_amount: vault.total_amount, cliff_duration, start_time };
- env.events().publish((Symbol::new(&env, "VaultCreated"), vault_id), vault_created);
+ let vault_created = VaultCreated {
+ vault_id,
+ beneficiary: vault.owner.clone(),
+ total_amount: vault.total_amount,
+ cliff_duration,
+ start_time,
+ };
+ env.events()
+ .publish((Symbol::new(&env, "VaultCreated"), vault_id), vault_created);
}
- let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
+ let mut total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
total_shares += total_amount;
- env.storage().instance().set(&DataKey::TotalShares, &total_shares);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &total_shares);
let final_count = initial_count + batch_data.recipients.len() as u64;
- env.storage().instance().set(&DataKey::VaultCount, &final_count);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultCount, &final_count);
vault_ids
}
@@ -921,19 +1127,32 @@ impl VestingContract {
Self::require_admin(&env);
let mut vault_ids = Vec::new(&env);
- let initial_count: u64 = env.storage().instance().get(&DataKey::VaultCount).unwrap_or(0);
+ let initial_count: u64 = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultCount)
+ .unwrap_or(0);
let total_amount: i128 = batch_data.amounts.iter().sum();
- let mut admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
- if admin_balance < total_amount { panic!("Insufficient admin balance for batch"); }
- admin_balance -= total_amount;
- env.storage().instance().set(&DataKey::AdminBalance, &admin_balance);
+ let mut admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
+ if admin_balance < total_amount {
+ panic!("Insufficient admin balance for batch");
+ }
+ admin_balance -= total_amount;
+ env.storage()
+ .instance()
+ .set(&DataKey::AdminBalance, &admin_balance);
let now = env.ledger().timestamp();
for i in 0..batch_data.recipients.len() {
let vault_id = initial_count + i as u64 + 1;
let vault = Vault {
+ title: String::from_slice(&env, ""),
owner: batch_data.recipients.get(i).unwrap(),
delegate: None,
total_amount: batch_data.amounts.get(i).unwrap(),
@@ -947,29 +1166,52 @@ impl VestingContract {
is_transferable: false,
step_duration: batch_data.step_durations.get(i).unwrap_or(0),
staked_amount: 0,
- is_frozen: false,
+ is_frozen: false,
};
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- let mut user_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(vault.owner.clone())).unwrap_or(Vec::new(&env));
+ let mut user_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(vault.owner.clone()))
+ .unwrap_or(Vec::new(&env));
user_vaults.push_back(vault_id);
- env.storage().instance().set(&DataKey::UserVaults(vault.owner.clone()), &user_vaults);
+ env.storage()
+ .instance()
+ .set(&DataKey::UserVaults(vault.owner.clone()), &user_vaults);
vault_ids.push_back(vault_id);
let start_time = batch_data.start_times.get(i).unwrap();
let cliff_duration = start_time.saturating_sub(now);
- let vault_created = VaultCreated { vault_id, beneficiary: vault.owner.clone(), total_amount: vault.total_amount, cliff_duration, start_time };
- env.events().publish((Symbol::new(&env, "VaultCreated"), vault_id), vault_created);
+ let vault_created = VaultCreated {
+ vault_id,
+ beneficiary: vault.owner.clone(),
+ total_amount: vault.total_amount,
+ cliff_duration,
+ start_time,
+ };
+ env.events()
+ .publish((Symbol::new(&env, "VaultCreated"), vault_id), vault_created);
}
- let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
+ let mut total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
total_shares += total_amount;
- env.storage().instance().set(&DataKey::TotalShares, &total_shares);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &total_shares);
let final_count = initial_count + batch_data.recipients.len() as u64;
- env.storage().instance().set(&DataKey::VaultCount, &final_count);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultCount, &final_count);
vault_ids
}
@@ -980,18 +1222,24 @@ impl VestingContract {
.instance()
.get(&DataKey::VaultData(vault_id))
.unwrap_or_else(|| panic!("Vault not found"));
- let vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
if !vault.is_initialized {
Self::initialize_vault_metadata(&env, vault_id);
- env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap()
+ env.storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"))
} else {
vault
}
}
pub fn get_user_vaults(env: Env, user: Address) -> Vec {
- let vault_ids: Vec = env.storage().instance().get(&DataKey::UserVaults(user.clone())).unwrap_or(Vec::new(&env));
+ let vault_ids: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(user))
+ .unwrap_or(Vec::new(&env));
for vault_id in vault_ids.iter() {
let vault: Vault = env
@@ -999,7 +1247,6 @@ impl VestingContract {
.instance()
.get(&DataKey::VaultData(vault_id))
.unwrap_or_else(|| panic!("Vault not found"));
- let vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
if !vault.is_initialized {
Self::initialize_vault_metadata(&env, vault_id);
@@ -1012,30 +1259,26 @@ impl VestingContract {
pub fn revoke_tokens(env: Env, vault_id: u64) -> i128 {
Self::require_admin(&env);
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let mut vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
- if vault.is_irrevocable { panic!("Vault is irrevocable"); }
+ if vault.is_irrevocable {
+ panic!("Vault is irrevocable");
+ }
- let unreleased_amount = vault.total_amount - vault.released_amount;
- if unreleased_amount <= 0 { panic!("No tokens available to revoke"); }
+ let returned = vault.total_amount - vault.released_amount;
+ if returned <= 0 {
+ panic!("No tokens available to revoke");
+ }
vault.released_amount = vault.total_amount;
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
-
- let mut admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
- admin_balance += unreleased_amount;
- env.storage().instance().set(&DataKey::AdminBalance, &admin_balance);
-
- unreleased_amount
- }
-
- // Admin-only: Revoke tokens from a vault and return them to admin
- pub fn revoke_tokens(env: Env, vault_id: u64) -> i128 {
- Self::require_admin(&env);
-
- let returned = Self::internal_revoke_full(&env, vault_id);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- // Single admin balance update for this call
let mut admin_balance: i128 = env
.storage()
.instance()
@@ -1045,10 +1288,16 @@ impl VestingContract {
env.storage()
.instance()
.set(&DataKey::AdminBalance, &admin_balance);
- returned
- let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
- total_shares -= unreleased_amount;
- env.storage().instance().set(&DataKey::TotalShares, &total_shares);
+
+ let mut total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
+ total_shares -= returned;
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &total_shares);
let timestamp = env.ledger().timestamp();
env.events().publish(
@@ -1057,28 +1306,59 @@ impl VestingContract {
);
returned
- env.events().publish((Symbol::new(&env, "TokensRevoked"), vault_id), (unreleased_amount, timestamp));
-
- unreleased_amount
}
pub fn revoke_partial(env: Env, vault_id: u64, amount: i128) -> i128 {
Self::require_admin(&env);
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let mut vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
- if vault.is_irrevocable { panic!("Vault is irrevocable"); }
+ if vault.is_irrevocable {
+ panic!("Vault is irrevocable");
+ }
let unvested_balance = vault.total_amount - vault.released_amount;
- if amount <= 0 { panic!("Amount to revoke must be positive"); }
- if amount > unvested_balance { panic!("Amount exceeds unvested balance"); }
+ if amount <= 0 {
+ panic!("Amount to revoke must be positive");
+ }
+ if amount > unvested_balance {
+ panic!("Amount exceeds unvested balance");
+ }
vault.released_amount += amount;
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- let mut admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
+ let mut admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
admin_balance += amount;
- env.storage().instance().set(&DataKey::AdminBalance, &admin_balance);
+ env.storage()
+ .instance()
+ .set(&DataKey::AdminBalance, &admin_balance);
+
+ let mut total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
+ total_shares -= amount;
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &total_shares);
+
+ let timestamp = env.ledger().timestamp();
+ env.events().publish(
+ (Symbol::new(&env, "TokensRevoked"), vault_id),
+ (amount, timestamp),
+ );
amount
}
@@ -1088,12 +1368,35 @@ impl VestingContract {
Self::require_admin(&env);
let mut total_returned: i128 = 0;
- for id in vault_ids.iter() {
- let returned = Self::internal_revoke_full(&env, id);
+ for vault_id in vault_ids.iter() {
+ let mut vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
+
+ if vault.is_irrevocable {
+ panic!("Vault is irrevocable");
+ }
+
+ let returned = vault.total_amount - vault.released_amount;
+ if returned <= 0 {
+ continue;
+ }
+
+ vault.released_amount = vault.total_amount;
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
total_returned += returned;
+
+ let timestamp = env.ledger().timestamp();
+ env.events().publish(
+ (Symbol::new(&env, "TokensRevoked"), vault_id),
+ (returned, timestamp),
+ );
}
- // Single admin balance update for the whole batch
let mut admin_balance: i128 = env
.storage()
.instance()
@@ -1103,61 +1406,101 @@ impl VestingContract {
env.storage()
.instance()
.set(&DataKey::AdminBalance, &admin_balance);
- total_returned
- let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
- total_shares -= amount;
- env.storage().instance().set(&DataKey::TotalShares, &total_shares);
+
+ let mut total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
+ total_shares -= total_returned;
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &total_shares);
let timestamp = env.ledger().timestamp();
env.events().publish(
- (Symbol::new(&env, "BatchRevoked"), vault_ids.len()),
- (total_returned, timestamp),
+ (Symbol::new(&env, "BatchRevoked"),),
+ (vault_ids.len(), total_returned, timestamp),
);
total_returned
- env.events().publish((Symbol::new(&env, "TokensRevoked"), vault_id), (amount, timestamp));
-
- amount
}
pub fn clawback_vault(env: Env, vault_id: u64) -> i128 {
Self::require_admin(&env);
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let mut vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
let now = env.ledger().timestamp();
let grace_period = 3600u64;
- if now > vault.creation_time + grace_period { panic!("Grace period expired"); }
- if vault.released_amount > 0 { panic!("Tokens already claimed"); }
+ if now > vault.creation_time + grace_period {
+ panic!("Grace period expired");
+ }
+ if vault.released_amount > 0 {
+ panic!("Tokens already claimed");
+ }
- let mut admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
+ let mut admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
admin_balance += vault.total_amount;
- env.storage().instance().set(&DataKey::AdminBalance, &admin_balance);
+ env.storage()
+ .instance()
+ .set(&DataKey::AdminBalance, &admin_balance);
vault.released_amount = vault.total_amount;
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
+ let mut total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
total_shares -= vault.total_amount;
- env.storage().instance().set(&DataKey::TotalShares, &total_shares);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &total_shares);
- env.events().publish((Symbol::new(&env, "VaultClawedBack"), vault_id), vault.total_amount);
+ env.events().publish(
+ (Symbol::new(&env, "VaultClawedBack"), vault_id),
+ vault.total_amount,
+ );
vault.total_amount
}
pub fn transfer_vault(env: Env, vault_id: u64, new_beneficiary: Address) {
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let mut vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
- if !vault.is_initialized { panic!("Vault not initialized"); }
- if !vault.is_transferable { panic!("Vault is non-transferable"); }
+ if !vault.is_initialized {
+ panic!("Vault not initialized");
+ }
+ if !vault.is_transferable {
+ panic!("Vault is non-transferable");
+ }
vault.owner.require_auth();
let old_owner = vault.owner.clone();
- let old_user_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(old_owner.clone())).unwrap_or(Vec::new(&env));
+ let old_user_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(old_owner.clone()))
+ .unwrap_or(Vec::new(&env));
let mut new_old_user_vaults = Vec::new(&env);
for id in old_user_vaults.iter() {
@@ -1165,29 +1508,54 @@ impl VestingContract {
new_old_user_vaults.push_back(id);
}
}
- env.storage().instance().set(&DataKey::UserVaults(old_owner.clone()), &new_old_user_vaults);
+ env.storage().instance().set(
+ &DataKey::UserVaults(old_owner.clone()),
+ &new_old_user_vaults,
+ );
- let mut new_user_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(new_beneficiary.clone())).unwrap_or(Vec::new(&env));
+ let mut new_user_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(new_beneficiary.clone()))
+ .unwrap_or(Vec::new(&env));
new_user_vaults.push_back(vault_id);
- env.storage().instance().set(&DataKey::UserVaults(new_beneficiary.clone()), &new_user_vaults);
+ env.storage().instance().set(
+ &DataKey::UserVaults(new_beneficiary.clone()),
+ &new_user_vaults,
+ );
vault.owner = new_beneficiary.clone();
vault.delegate = None;
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- env.events().publish((Symbol::new(&env, "BeneficiaryUpdated"), vault_id), (old_owner, new_beneficiary));
+ env.events().publish(
+ (Symbol::new(&env, "BeneficiaryUpdated"), vault_id),
+ (old_owner, new_beneficiary),
+ );
}
pub fn rotate_beneficiary_key(env: Env, vault_id: u64, new_address: Address) {
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let mut vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
- if !vault.is_initialized { panic!("Vault not initialized"); }
+ if !vault.is_initialized {
+ panic!("Vault not initialized");
+ }
vault.owner.require_auth();
let old_owner = vault.owner.clone();
- let old_user_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(old_owner.clone())).unwrap_or(Vec::new(&env));
+ let old_user_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(old_owner.clone()))
+ .unwrap_or(Vec::new(&env));
let mut new_old_user_vaults = Vec::new(&env);
for id in old_user_vaults.iter() {
@@ -1195,22 +1563,38 @@ impl VestingContract {
new_old_user_vaults.push_back(id);
}
}
- env.storage().instance().set(&DataKey::UserVaults(old_owner.clone()), &new_old_user_vaults);
+ env.storage().instance().set(
+ &DataKey::UserVaults(old_owner.clone()),
+ &new_old_user_vaults,
+ );
- let mut new_user_vaults: Vec = env.storage().instance().get(&DataKey::UserVaults(new_address.clone())).unwrap_or(Vec::new(&env));
+ let mut new_user_vaults: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserVaults(new_address.clone()))
+ .unwrap_or(Vec::new(&env));
new_user_vaults.push_back(vault_id);
- env.storage().instance().set(&DataKey::UserVaults(new_address.clone()), &new_user_vaults);
+ env.storage()
+ .instance()
+ .set(&DataKey::UserVaults(new_address.clone()), &new_user_vaults);
vault.owner = new_address.clone();
vault.delegate = None;
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
- env.events().publish((Symbol::new(&env, "BeneficiaryRotated"), vault_id), (old_owner, new_address));
+ env.events().publish(
+ (Symbol::new(&env, "BeneficiaryRotated"), vault_id),
+ (old_owner, new_address),
+ );
}
pub fn set_staking_contract(env: Env, contract: Address) {
Self::require_admin(&env);
- env.storage().instance().set(&Symbol::new(&env, "StakingContract"), &contract);
+ env.storage()
+ .instance()
+ .set(&Symbol::new(&env, "StakingContract"), &contract);
}
pub fn stake_tokens(env: Env, vault_id: u64, amount: i128, validator: Address) {
@@ -1220,58 +1604,107 @@ impl VestingContract {
.instance()
.get(&DataKey::VaultData(vault_id))
.unwrap_or_else(|| panic!("Vault not found"));
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
- if !vault.is_initialized { panic!("Vault not initialized"); }
+ if !vault.is_initialized {
+ panic!("Vault not initialized");
+ }
vault.owner.require_auth();
let available = vault.total_amount - vault.released_amount - vault.staked_amount;
- if amount <= 0 { panic!("Amount must be positive"); }
- if amount > available { panic!("Insufficient funds to stake"); }
-
- let staking_contract: Address = env.storage().instance().get(&Symbol::new(&env, "StakingContract")).expect("Staking contract not set");
+ if amount <= 0 {
+ panic!("Amount must be positive");
+ }
+ if amount > available {
+ panic!("Insufficient funds to stake");
+ }
- let args = vec![&env, vault_id.into_val(&env), amount.into_val(&env), validator.into_val(&env)];
+ let staking_contract: Address = env
+ .storage()
+ .instance()
+ .get(&Symbol::new(&env, "StakingContract"))
+ .expect("Staking contract not set");
+
+ let args = vec![
+ &env,
+ vault_id.into_val(&env),
+ amount.into_val(&env),
+ validator.into_val(&env),
+ ];
env.invoke_contract::<()>(&staking_contract, &Symbol::new(&env, "stake"), args);
vault.staked_amount += amount;
- let mut total_staked: i128 = env.storage().instance().get(&DataKey::TotalStaked).unwrap_or(0);
+ let mut total_staked: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalStaked)
+ .unwrap_or(0);
total_staked += amount;
- env.storage().instance().set(&DataKey::TotalStaked, &total_staked);
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalStaked, &total_staked);
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
}
pub fn mark_irrevocable(env: Env, vault_id: u64) {
Self::require_admin(&env);
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let mut vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
- if vault.is_irrevocable { panic!("Vault is already irrevocable"); }
+ if vault.is_irrevocable {
+ panic!("Vault is already irrevocable");
+ }
vault.is_irrevocable = true;
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
+ env.storage()
+ .instance()
+ .set(&DataKey::VaultData(vault_id), &vault);
let timestamp = env.ledger().timestamp();
- env.events().publish((Symbol::new(&env, "IrrevocableMarked"), vault_id), timestamp);
+ env.events().publish(
+ (Symbol::new(&env, "IrrevocableMarked"), vault_id),
+ timestamp,
+ );
}
pub fn is_vault_irrevocable(env: Env, vault_id: u64) -> bool {
- let vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
+ let vault: Vault = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultData(vault_id))
+ .unwrap_or_else(|| panic!("Vault not found"));
vault.is_irrevocable
}
pub fn get_contract_state(env: Env) -> (i128, i128, i128) {
- let admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
+ let admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
- let vault_count: u64 = env.storage().instance().get(&DataKey::VaultCount).unwrap_or(0);
+ let vault_count: u64 = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultCount)
+ .unwrap_or(0);
let mut total_locked = 0i128;
let mut total_claimed = 0i128;
for i in 1..=vault_count {
- if let Some(vault) = env.storage().instance().get::(&DataKey::VaultData(i)) {
+ if let Some(vault) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::VaultData(i))
+ {
total_locked += vault.total_amount - vault.released_amount;
total_claimed += vault.released_amount;
}
@@ -1281,7 +1714,11 @@ impl VestingContract {
}
pub fn check_invariant(env: Env) -> bool {
- let initial_supply: i128 = env.storage().instance().get(&DataKey::InitialSupply).unwrap_or(0);
+ let initial_supply: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::InitialSupply)
+ .unwrap_or(0);
let (total_locked, _total_claimed, admin_balance) = Self::get_contract_state(env);
let net_paid_out = initial_supply - total_locked - admin_balance;
@@ -1289,13 +1726,11 @@ impl VestingContract {
}
pub fn get_claimable_amount(env: Env, vault_id: u64) -> i128 {
- let vault: Vault = env.storage().instance()
let vault: Vault = env
.storage()
.instance()
.get(&DataKey::VaultData(vault_id))
.unwrap_or_else(|| panic!("Vault not found"));
- let vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
let vested = Self::calculate_time_vested_amount(&env, &vault);
@@ -1307,7 +1742,6 @@ impl VestingContract {
}
pub fn auto_claim(env: Env, vault_id: u64, keeper: Address) {
- let mut vault: Vault = env.storage().instance()
if Self::is_paused(env.clone()) {
panic!("Contract is paused - all withdrawals are disabled");
}
@@ -1317,7 +1751,6 @@ impl VestingContract {
.instance()
.get(&DataKey::VaultData(vault_id))
.unwrap_or_else(|| panic!("Vault not found"));
- let mut vault: Vault = env.storage().instance().get(&DataKey::VaultData(vault_id)).unwrap_or_else(|| panic!("Vault not found"));
// Check if vault is frozen
if vault.is_frozen {
@@ -1327,11 +1760,12 @@ impl VestingContract {
if !vault.is_initialized {
panic!("Vault not initialized");
}
- if !vault.is_initialized { panic!("Vault not initialized"); }
let claimable = Self::get_claimable_amount(env.clone(), vault_id);
- if claimable <= vault.keeper_fee { panic!("Insufficient claimable tokens to cover fee"); }
+ if claimable <= vault.keeper_fee {
+ panic!("Insufficient claimable tokens to cover fee");
+ }
let beneficiary_amount = claimable - vault.keeper_fee;
let keeper_fee = vault.keeper_fee;
@@ -1339,10 +1773,22 @@ impl VestingContract {
// YIELD DISTRIBUTION - only vault-owned portion
let token_client = Self::get_token_client(&env);
let current_balance = token_client.balance(&env.current_contract_address());
- let admin_balance: i128 = env.storage().instance().get(&DataKey::AdminBalance).unwrap_or(0);
+ let admin_balance: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminBalance)
+ .unwrap_or(0);
- let total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0);
- let total_staked: i128 = env.storage().instance().get(&DataKey::TotalStaked).unwrap_or(0);
+ let total_shares: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalShares)
+ .unwrap_or(0);
+ let total_staked: i128 = env
+ .storage()
+ .instance()
+ .get(&DataKey::TotalStaked)
+ .unwrap_or(0);
let liquid_shares = total_shares - total_staked;
let vault_portion = (current_balance - admin_balance).max(0);
@@ -1359,27 +1805,43 @@ impl VestingContract {
};
vault.released_amount += claimable;
+ let mut updated_total_shares = total_shares;
+ updated_total_shares -= claimable;
+ env.storage()
+ .instance()
+ .set(&DataKey::TotalShares, &updated_total_shares);
env.storage()
.instance()
.set(&DataKey::VaultData(vault_id), &vault);
- let mut updated_total_shares = total_shares;
- updated_total_shares -= claimable;
- env.storage().instance().set(&DataKey::TotalShares, &updated_total_shares);
- env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);
- token_client.transfer(&env.current_contract_address(), &vault.owner, &beneficiary_tokens);
+ token_client.transfer(
+ &env.current_contract_address(),
+ &vault.owner,
+ &beneficiary_tokens,
+ );
token_client.transfer(&env.current_contract_address(), &keeper, &keeper_tokens);
- let mut fees: Map = env.storage().instance().get(&DataKey::KeeperFees).unwrap_or(Map::new(&env));
+ let mut fees: Map = env
+ .storage()
+ .instance()
+ .get(&DataKey::KeeperFees)
+ .unwrap_or(Map::new(&env));
let current_fees = fees.get(keeper.clone()).unwrap_or(0);
fees.set(keeper.clone(), current_fees + keeper_fee);
env.storage().instance().set(&DataKey::KeeperFees, &fees);
- env.events().publish((Symbol::new(&env, "KeeperClaim"), vault_id), (keeper, beneficiary_amount, keeper_fee));
+ env.events().publish(
+ (Symbol::new(&env, "KeeperClaim"), vault_id),
+ (keeper, beneficiary_amount, keeper_fee),
+ );
}
pub fn get_keeper_fee(env: Env, keeper: Address) -> i128 {
- let fees: Map = env.storage().instance().get(&DataKey::KeeperFees).unwrap_or(Map::new(&env));
+ let fees: Map = env
+ .storage()
+ .instance()
+ .get(&DataKey::KeeperFees)
+ .unwrap_or(Map::new(&env));
fees.get(keeper).unwrap_or(0)
}
@@ -1399,11 +1861,19 @@ impl VestingContract {
}
}
- let vault_count: u64 = env.storage().instance().get(&DataKey::VaultCount).unwrap_or(0);
+ let vault_count: u64 = env
+ .storage()
+ .instance()
+ .get(&DataKey::VaultCount)
+ .unwrap_or(0);
let mut total_liabilities: i128 = 0;
for i in 1..=vault_count {
- if let Some(vault) = env.storage().instance().get::(&DataKey::VaultData(i)) {
+ if let Some(vault) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::VaultData(i))
+ {
let unreleased = vault.total_amount - vault.released_amount;
if unreleased > 0 {
total_liabilities += unreleased;
@@ -1417,15 +1887,23 @@ impl VestingContract {
panic!("No unallocated tokens to rescue");
}
- let admin: Address = env.storage().instance().get(&DataKey::AdminAddress).unwrap_or_else(|| panic!("Admin not set"));
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::AdminAddress)
+ .unwrap_or_else(|| panic!("Admin not set"));
- token_client.transfer(&env.current_contract_address(), &admin, &unallocated_balance);
+ token_client.transfer(
+ &env.current_contract_address(),
+ &admin,
+ &unallocated_balance,
+ );
- env.events().publish((Symbol::new(&env, "RescueExecuted"), token_address), (unallocated_balance, admin));
+ env.events().publish(
+ (Symbol::new(&env, "RescueExecuted"), token_address),
+ (unallocated_balance, admin),
+ );
unallocated_balance
}
}
-
-// // mod test; // Disabled - tests need refactoring
-mod test;
diff --git a/contracts/vesting_contracts/tests/multisig_admin_threshold.rs b/contracts/vesting_contracts/tests/multisig_admin_threshold.rs
new file mode 100644
index 0000000..df39f21
--- /dev/null
+++ b/contracts/vesting_contracts/tests/multisig_admin_threshold.rs
@@ -0,0 +1,273 @@
+use soroban_sdk::auth::{Context, CustomAccountInterface};
+use soroban_sdk::crypto::Hash;
+use soroban_sdk::testutils::{Address as _, Ledger as _};
+use soroban_sdk::xdr;
+use soroban_sdk::{
+ contract, contracterror, contractimpl, contracttype, Address, Env, IntoVal, Map, Symbol, Val,
+ Vec,
+};
+
+use vesting_contracts::{VestingContract, VestingContractClient};
+
+#[contract]
+struct MultisigAccount;
+
+#[contracttype]
+enum MultisigDataKey {
+ Signers,
+ Threshold,
+}
+
+#[contracterror]
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+enum MultisigError {
+ ThresholdNotMet = 1,
+ InvalidContext = 2,
+}
+
+#[contractimpl]
+impl MultisigAccount {
+ pub fn init(env: Env, signers: Vec, threshold: u32) {
+ env.storage()
+ .instance()
+ .set(&MultisigDataKey::Signers, &signers);
+ env.storage()
+ .instance()
+ .set(&MultisigDataKey::Threshold, &threshold);
+ }
+}
+
+#[contractimpl]
+impl CustomAccountInterface for MultisigAccount {
+ type Signature = Vec;
+ type Error = MultisigError;
+
+ fn __check_auth(
+ env: Env,
+ _signature_payload: Hash<32>,
+ signatures: Vec,
+ auth_contexts: Vec,
+ ) -> Result<(), Self::Error> {
+ let allowed: Vec = env
+ .storage()
+ .instance()
+ .get(&MultisigDataKey::Signers)
+ .unwrap_or(Vec::new(&env));
+ let threshold: u32 = env
+ .storage()
+ .instance()
+ .get(&MultisigDataKey::Threshold)
+ .unwrap_or(0);
+
+ let mut allowed_map: Map = Map::new(&env);
+ for addr in allowed.iter() {
+ allowed_map.set(addr, true);
+ }
+
+ let mut seen: Map = Map::new(&env);
+ let mut approvals: u32 = 0;
+ for signer in signatures.iter() {
+ if allowed_map.get(signer.clone()).unwrap_or(false)
+ && !seen.get(signer.clone()).unwrap_or(false)
+ {
+ seen.set(signer.clone(), true);
+ approvals += 1;
+ }
+ }
+ if approvals < threshold {
+ return Err(MultisigError::ThresholdNotMet);
+ }
+
+ // Ensure we were asked to authorize a create_vault_full call.
+ let expected_fn = Symbol::new(&env, "create_vault_full");
+ let mut has_expected_context = false;
+ for ctx in auth_contexts.iter() {
+ if let Context::Contract(contract_ctx) = ctx {
+ if contract_ctx.fn_name == expected_fn {
+ has_expected_context = true;
+ break;
+ }
+ }
+ }
+ if !has_expected_context {
+ return Err(MultisigError::InvalidContext);
+ }
+
+ Ok(())
+ }
+}
+
+fn auth_entry_for_multisig(
+ env: &Env,
+ authorizer: &Address,
+ contract: &Address,
+ fn_name: &str,
+ args: Vec,
+ signature: xdr::ScVal,
+ nonce: i64,
+) -> xdr::SorobanAuthorizationEntry {
+ let root_invocation = xdr::SorobanAuthorizedInvocation {
+ function: xdr::SorobanAuthorizedFunction::ContractFn(xdr::InvokeContractArgs {
+ contract_address: contract.clone().try_into().unwrap(),
+ function_name: fn_name.try_into().unwrap(),
+ args: args.try_into().unwrap(),
+ }),
+ sub_invocations: std::vec::Vec::::new()
+ .try_into()
+ .unwrap(),
+ };
+
+ xdr::SorobanAuthorizationEntry {
+ root_invocation,
+ credentials: xdr::SorobanCredentials::Address(xdr::SorobanAddressCredentials {
+ address: authorizer.try_into().unwrap(),
+ nonce,
+ signature_expiration_ledger: env.ledger().sequence() + 1000,
+ signature,
+ }),
+ }
+}
+
+fn signatures_scval(signers: &[Address]) -> xdr::ScVal {
+ let mut sig_vals: std::vec::Vec = std::vec::Vec::with_capacity(signers.len());
+ for signer in signers {
+ sig_vals.push(xdr::ScVal::Address(signer.try_into().unwrap()));
+ }
+ xdr::ScVal::Vec(Some(sig_vals.try_into().unwrap()))
+}
+
+#[test]
+fn create_vault_succeeds_with_multisig_admin_threshold_met() {
+ let env = Env::default();
+ env.ledger().set_sequence_number(1);
+ env.ledger().set_timestamp(1_000);
+
+ // Multisig admin custom account.
+ let multisig_id = env.register(MultisigAccount, ());
+ let multisig_client = MultisigAccountClient::new(&env, &multisig_id);
+
+ let s1 = Address::generate(&env);
+ let s2 = Address::generate(&env);
+ let s3 = Address::generate(&env);
+ let mut signers = Vec::new(&env);
+ signers.push_back(s1.clone());
+ signers.push_back(s2.clone());
+ signers.push_back(s3.clone());
+ multisig_client.init(&signers, &2u32);
+
+ // Vesting contract with multisig as admin.
+ let vesting_id = env.register(VestingContract, ());
+ let vesting = VestingContractClient::new(&env, &vesting_id);
+ vesting.initialize(&multisig_id, &1_000_000i128);
+
+ let beneficiary = Address::generate(&env);
+ let now = env.ledger().timestamp();
+ let amount = 1_000i128;
+ let keeper_fee = 0i128;
+ let start = now;
+ let end = now + 1_000;
+ let is_revocable = true;
+ let is_transferable = false;
+ let step_duration = 0u64;
+
+ // Provide an authorization entry for the multisig admin where signatures contain >= threshold signers.
+ let args: Vec = (
+ beneficiary.clone(),
+ amount,
+ start,
+ end,
+ keeper_fee,
+ is_revocable,
+ is_transferable,
+ step_duration,
+ )
+ .into_val(&env);
+ let entry = auth_entry_for_multisig(
+ &env,
+ &multisig_id,
+ &vesting_id,
+ "create_vault_full",
+ args,
+ signatures_scval(&[s1.clone(), s2.clone()]),
+ 1,
+ );
+ env.set_auths(&[entry]);
+
+ let vault_id = vesting.create_vault_full(
+ &beneficiary,
+ &amount,
+ &start,
+ &end,
+ &keeper_fee,
+ &is_revocable,
+ &is_transferable,
+ &step_duration,
+ );
+ assert_eq!(vault_id, 1u64);
+}
+
+#[test]
+#[should_panic]
+fn create_vault_panics_when_multisig_threshold_not_met() {
+ let env = Env::default();
+ env.ledger().set_sequence_number(1);
+ env.ledger().set_timestamp(1_000);
+
+ let multisig_id = env.register(MultisigAccount, ());
+ let multisig_client = MultisigAccountClient::new(&env, &multisig_id);
+
+ let s1 = Address::generate(&env);
+ let s2 = Address::generate(&env);
+ let mut signers = Vec::new(&env);
+ signers.push_back(s1.clone());
+ signers.push_back(s2.clone());
+ multisig_client.init(&signers, &2u32);
+
+ let vesting_id = env.register(VestingContract, ());
+ let vesting = VestingContractClient::new(&env, &vesting_id);
+ vesting.initialize(&multisig_id, &1_000_000i128);
+
+ let beneficiary = Address::generate(&env);
+ let now = env.ledger().timestamp();
+ let amount = 1_000i128;
+ let keeper_fee = 0i128;
+ let start = now;
+ let end = now + 1_000;
+ let is_revocable = true;
+ let is_transferable = false;
+ let step_duration = 0u64;
+
+ // Only one signer provided, but threshold is 2.
+ let args: Vec = (
+ beneficiary.clone(),
+ amount,
+ start,
+ end,
+ keeper_fee,
+ is_revocable,
+ is_transferable,
+ step_duration,
+ )
+ .into_val(&env);
+ let entry = auth_entry_for_multisig(
+ &env,
+ &multisig_id,
+ &vesting_id,
+ "create_vault_full",
+ args,
+ signatures_scval(&[s1.clone()]),
+ 1,
+ );
+ env.set_auths(&[entry]);
+
+ vesting.create_vault_full(
+ &beneficiary,
+ &amount,
+ &start,
+ &end,
+ &keeper_fee,
+ &is_revocable,
+ &is_transferable,
+ &step_duration,
+ );
+}