Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 132 additions & 5 deletions contracts/vesting_contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,25 @@ pub enum DataKey {
MetadataAnchor,
VotingDelegate(Address),
DelegatedBeneficiaries(Address),
SubAdminPool(Address),
MarketplaceLock(u64),
XLMAddress,
}

#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct SubAdminPool {
pub manager: Address,
pub asset: Address,
pub total_amount: i128,
pub distributed_amount: i128,
}

#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct MarketplaceLock {
pub marketplace: Address,
pub authorized_at: u64,
}

#[contracttype]
Expand All @@ -116,6 +135,9 @@ pub enum AdminAction {
// Anti-Dilution Configuration
AntiDilutionConfig(u64),
NetworkGrowthSnapshot(u64),
GrantManagerRights(Address, Address, i128), // Manager, Asset, Amount
RenewSchedule(u64, u64, i128), // VaultID, AdditionalDuration, AdditionalAmount
SetXLMAddress(Address),
}

#[contracttype]
Expand Down Expand Up @@ -362,6 +384,25 @@ impl VestingContract {
true,
);
},
AdminAction::GrantManagerRights(manager, asset, amount) => {
let pool = SubAdminPool {
manager: manager.clone(),
asset: asset.clone(),
total_amount: amount,
distributed_amount: 0,
};
env.storage().instance().set(&DataKey::SubAdminPool(manager), &pool);

// Transfer tokens from admin to contract to fund the pool
let admin = Self::get_admin(env.clone());
token::Client::new(&env, &asset).transfer(&admin, &env.current_contract_address(), &amount);
},
AdminAction::RenewSchedule(vault_id, duration, amount) => {
Self::do_renew_vault_direct(&env, vault_id, duration, amount);
},
AdminAction::SetXLMAddress(xlm) => {
env.storage().instance().set(&DataKey::XLMAddress, &xlm);
},
_ => {
// For other actions, no-op or extend as needed
}
Expand Down Expand Up @@ -976,7 +1017,20 @@ impl VestingContract {
// Calculate and claim each asset in the basket
for (i, allocation) in vault.allocations.iter().enumerate() {
let vested_amount = Self::calculate_claimable_for_asset(&env, vault_id, &vault, i);
let claimable_amount = vested_amount - allocation.released_amount;
let mut claimable_amount = vested_amount - allocation.released_amount;

// #90: XLM Minimum Reserve Check (2 XLM = 20,000,000 Stroops)
let xlm: Option<Address> = env.storage().instance().get(&DataKey::XLMAddress);
if let Some(xlm_addr) = xlm {
if allocation.asset_id == xlm_addr {
let total_unreleased = allocation.total_amount - allocation.released_amount;
if total_unreleased <= 20_000_000 {
claimable_amount = 0;
} else if (total_unreleased - claimable_amount) < 20_000_000 {
claimable_amount = total_unreleased - 20_000_000;
}
}
}

if claimable_amount > 0 {
// Update the allocation's released amount
Expand Down Expand Up @@ -1045,10 +1099,17 @@ impl VestingContract {

env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);

let token: Address = env.storage().instance().get(&DataKey::Token).expect("Token not set");
let contract_addr = env.current_contract_address();
token::Client::new(&env, &token).transfer(&contract_addr, &vault.owner, &claim_amount);

// #90: XLM Minimum Reserve Check
let xlm: Option<Address> = env.storage().instance().get(&DataKey::XLMAddress);
if let Some(xlm_addr) = xlm {
if allocation.asset_id == xlm_addr {
let total_left = allocation.total_amount - (allocation.released_amount + claim_amount);
if total_left < 20_000_000 {
panic!("Claim would leave insufficient XLM for gas (need 2 XLM reserve)");
}
}
}

token::Client::new(&env, &allocation.asset_id)
.transfer(&env.current_contract_address(), &vault.owner, &claim_amount);

Expand Down Expand Up @@ -2635,6 +2696,72 @@ impl VestingContract {
pub fn get_total_locked(env: Env) -> i128 {
VestingContract::get_total_locked_value(&env)
}

// --- Marketplace Functions (#89) ---

pub fn authorize_transfer_to_marketplace(env: Env, vault_id: u64, marketplace: Address) {
let vault = Self::get_vault_internal(&env, vault_id);
vault.owner.require_auth();
if !vault.is_transferable {
panic!("Vault not transferable");
}
let lock = MarketplaceLock {
marketplace,
authorized_at: env.ledger().timestamp(),
};
env.storage().instance().set(&DataKey::MarketplaceLock(vault_id), &lock);
}

pub fn complete_marketplace_transfer(env: Env, vault_id: u64, new_owner: Address) {
let lock: MarketplaceLock = env.storage().instance().get(&DataKey::MarketplaceLock(vault_id)).expect("Vault not authorized for marketplace");
lock.marketplace.require_auth();

let mut vault = Self::get_vault_internal(&env, vault_id);
let old_owner = vault.owner.clone();

// Update owner
vault.owner = new_owner.clone();
env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);

// Update indexes
Self::remove_user_vault_index(&env, &old_owner, vault_id);
Self::add_user_vault_index(&env, &new_owner, vault_id);

// Clear lock
env.storage().instance().remove(&DataKey::MarketplaceLock(vault_id));

env.events().publish(
(Symbol::new(&env, "vault_marketplace_sold"), vault_id),
(old_owner, new_owner, lock.marketplace),
);
}

// --- Renewal Functions (#91) ---

fn do_renew_vault_direct(env: &Env, vault_id: u64, additional_duration: u64, additional_amount: i128) {
let mut vault = Self::get_vault_internal(env, vault_id);

// Find main asset (first one)
let mut allocation = vault.allocations.get(0).expect("Empty basket");
let asset_id = allocation.asset_id.clone();

// Fund extra from admin
let admin = Self::get_admin(env.clone());
token::Client::new(env, &asset_id).transfer(&admin, &env.current_contract_address(), &additional_amount);

allocation.total_amount += additional_amount;
vault.allocations.set(0, allocation);
vault.end_time += additional_duration;

env.storage().instance().set(&DataKey::VaultData(vault_id), &vault);

env.events().publish((Symbol::new(env, "vault_renewed"), vault_id), (additional_duration, additional_amount));
}

pub fn renew_schedule(env: Env, vault_id: u64, additional_duration: u64, additional_amount: i128) {
Self::require_admin(&env);
Self::do_renew_vault_direct(&env, vault_id, additional_duration, additional_amount);
}
}

// Test modules temporarily disabled to allow iterative compilation while
Expand Down
129 changes: 129 additions & 0 deletions contracts/vesting_contracts/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1428,3 +1428,132 @@ fn test_full_happy_path_nominate_claim_finalise_new_owner_verified() {
panic!("Expected Succeeded state");
}
}
#[test]
fn test_sub_vault_delegation() {
let (env, _, client, admin, token) = setup();
let sub_admin = Address::generate(&env);
let team_member = Address::generate(&env);

// 1. Admin grants rights
// Note: We'll use the direct call for testing if allowed,
// but the implementation requires AdminProposal usually.
// In our tests mock_all_auths is on.

// We need to bypass the "panic!("Admin actions must be executed via AdminProposal...")"
// if we call the regular public methods that are gated.
// Actually, GrantManagerRights is only in dispatch_admin_action in my last edit.
// So I should proprose it.

let action = crate::AdminAction::GrantManagerRights(sub_admin.clone(), token.clone(), 50_000i128);
client.propose_admin_action(&admin, &action);

// 2. Sub-admin creates vault
let now = env.ledger().timestamp();
let vault_id = client.sub_admin_create_vault(
&sub_admin,
&team_member,
&10_000i128,
&now,
&(now + 1000),
&0i128,
&false,
&false,
&0u64,
&String::from_str(&env, "Team Lead's Vault")
);

assert_eq!(vault_id, 1);
let vault = client.get_vault(&vault_id);
assert_eq!(vault.owner, team_member);
assert_eq!(vault.delegate, Some(sub_admin.clone()));

// 3. Sub-admin revokes vault
client.sub_admin_revoke_vault(&sub_admin, &vault_id);
let updated_vault = client.get_vault(&vault_id);
assert!(updated_vault.is_frozen);
}

#[test]
fn test_marketplace_listing_and_sale() {
let (env, _, client, _admin, _) = setup();
let beneficiary = Address::generate(&env);
let marketplace = Address::generate(&env);
let buyer = Address::generate(&env);
let now = env.ledger().timestamp();

let vault_id = client.create_vault_full(
&beneficiary,
&1000i128,
&now,
&(now + 1000),
&0i128,
&false,
&true, // must be transferable
&0u64
);

// 1. Beneficiary authorizes marketplace
client.authorize_transfer_to_marketplace(&vault_id, &marketplace);

// 2. Marketplace completes transfer to buyer
client.complete_marketplace_transfer(&vault_id, &buyer);

let vault = client.get_vault(&vault_id);
assert_eq!(vault.owner, buyer);
}

#[test]
#[should_panic(expected = "Claim would leave insufficient XLM for gas (need 2 XLM reserve)")]
fn test_xlm_gas_reserve() {
let (env, _, client, admin, xlm_token) = setup();
let beneficiary = Address::generate(&env);
let now = env.ledger().timestamp();

// Set XLM address in contract
let action = crate::AdminAction::SetXLMAddress(xlm_token.clone());
client.propose_admin_action(&admin, &action);

// Create XLM vault with 5 XLM
let total_xlm = 50_000_000i128; // 5 XLM
let vault_id = client.create_vault_full(
&beneficiary,
&total_xlm,
&now,
&(now + 1000),
&0i128,
&false,
&false,
&0u64
);

// Fast forward to end
env.ledger().set_timestamp(now + 1000);

// Try to claim 4 XLM (leaving 1 XLM) - should fail
client.claim_tokens(&vault_id, &40_000_000i128);
}

#[test]
fn test_vault_renewal() {
let (env, _, client, admin, _) = setup();
let beneficiary = Address::generate(&env);
let now = env.ledger().timestamp();

let vault_id = client.create_vault_full(
&beneficiary,
&1000i128,
&now,
&(now + 1000),
&0i128,
&false,
&false,
&0u64
);

// Renew: add 1000 seconds and 500 tokens
client.renew_schedule(&vault_id, &1000u64, &500i128);

let vault = client.get_vault(&vault_id);
assert_eq!(vault.end_time, now + 2000);
assert_eq!(vault.allocations.get(0).unwrap().total_amount, 1500);
}
4 changes: 1 addition & 3 deletions target/.rustc_info.json
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
{"rustc_fingerprint":3924410247754035658,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\User\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.92.0 (ded5c06cf 2025-12-08)\nbinary: rustc\ncommit-hash: ded5c06cf21d2b93bffd5d884aa6e96934ee4234\ncommit-date: 2025-12-08\nhost: x86_64-pc-windows-msvc\nrelease: 1.92.0\nLLVM version: 21.1.3\n","stderr":""},"11652014622397750202":{"success":true,"status":"","code":0,"stdout":"___.wasm\nlib___.rlib\n___.wasm\nlib___.a\nC:\\Users\\User\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\noff\n___\ndebug_assertions\npanic=\"abort\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"wasm32\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"wasm\"\ntarget_feature=\"bulk-memory\"\ntarget_feature=\"multivalue\"\ntarget_feature=\"mutable-globals\"\ntarget_feature=\"nontrapping-fptoint\"\ntarget_feature=\"reference-types\"\ntarget_feature=\"sign-ext\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"unknown\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"unknown\"\n","stderr":"warning: dropping unsupported crate type `dylib` for target `wasm32-unknown-unknown`\n\nwarning: dropping unsupported crate type `proc-macro` for target `wasm32-unknown-unknown`\n\nwarning: 2 warnings emitted\n\n"}},"successes":{}}
{"rustc_fingerprint":11643027800151290351,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: aarch64-apple-darwin\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/mac/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}
{"rustc_fingerprint":12976345055248769160,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/mac/.rustup/toolchains/1.93.1-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}
{"rustc_fingerprint":7919731678241397688,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: x86_64-pc-windows-msvc\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""}},"successes":{}}