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
90 changes: 90 additions & 0 deletions contracts/split/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,12 @@ impl SplitContract {
claimed.push_back(0i128);
}

// Issue #27: Initialize vesting cliff claimed tracking (all false).
let mut vesting_cliff_claimed: Vec<bool> = Vec::new(env);
for _ in recipients.iter() {
vesting_cliff_claimed.push_back(false);
}

// Issue #87: Increment referral count if referrer is provided.
// (referrer is not yet wired into _create_invoice_inner; skipped)

Expand Down Expand Up @@ -911,6 +917,80 @@ impl SplitContract {
append_audit_entry(&env, invoice_id, symbol_short!("aprv"), approver);
}

/// Claim vesting cliff share after cliff timestamp has passed (issue #27).
///
/// Requires that the invoice status is Released and the cliff (if set) has passed.
/// Each recipient can claim exactly once.
pub fn claim(env: Env, invoice_id: u64, recipient: Address) {
require_not_paused(&env);
recipient.require_auth();

let mut invoice = load_invoice(&env, invoice_id);

assert!(
invoice.status == InvoiceStatus::Released,
"invoice not released"
);

// Find recipient index
let idx = invoice
.recipients
.iter()
.position(|r| r == recipient)
.expect("recipient not in invoice") as u32;

// Check if already claimed
assert!(
!invoice.vesting_cliff_claimed.get(idx).unwrap(),
"recipient already claimed"
);

// Check cliff timestamp if set
if let Some(cliff) = invoice.vesting_cliff {
let now = env.ledger().timestamp();
assert!(now >= cliff, "cliff not reached");
}

// Mark as claimed
invoice.vesting_cliff_claimed.set(idx, true);
save_invoice(&env, invoice_id, &invoice);

// Transfer recipient's share
let amount = invoice.amounts.get(idx).unwrap();
let total: i128 = invoice.amounts.iter().sum();
let funded = invoice.funded;
let n = invoice.recipients.len() as u32;

let proportional = if idx == n - 1 {
// Last recipient gets remainder
funded - {
let mut sum = 0i128;
for i in 0..idx {
let amt = invoice.amounts.get(i).unwrap();
let prop = (amt as u128 * funded as u128 / total as u128) as i128;
sum += prop;
}
sum
}
} else {
(amount as u128 * funded as u128 / total as u128) as i128
};

let platform_fee_bps: u32 = env
.storage()
.instance()
.get(&platform_fee_bps_key())
.unwrap_or(0u32);

let fee = (proportional as u128 * platform_fee_bps as u128 / 10_000u128) as i128;
let payout = proportional - fee;

let token_client = token::Client::new(&env, &invoice.tokens.get(idx).expect("no token"));
token_client.transfer(&env.current_contract_address(), &recipient, &payout);

append_audit_entry(&env, invoice_id, symbol_short!("claim"), &recipient);
}

/// Distribute tranches unlocked by the current ledger time (issue #23).
fn _release_tranches(env: &Env, invoice_id: u64, invoice: &mut Invoice, actor: &Address) {
let now = env.ledger().timestamp();
Expand Down Expand Up @@ -1119,6 +1199,16 @@ impl SplitContract {
/// Issue #89: Returns stake to creator on successful release.
/// Issue #41: Swaps recipient payout via DEX if swap_tokens[i] is set.
fn _release_full(env: &Env, invoice_id: u64, invoice: &mut Invoice, actor: &Address) {
// Issue #27: If vesting cliff is set, just mark as Released without transferring funds
if invoice.vesting_cliff.is_some() {
invoice.status = InvoiceStatus::Released;
invoice.completion_time = Some(env.ledger().timestamp());
save_invoice(env, invoice_id, invoice);
append_audit_entry(env, invoice_id, symbol_short!("release"), actor);
events::invoice_released(env, invoice_id, &invoice.recipients);
return;
}

let token_client =
token::Client::new(env, &invoice.tokens.get(0).expect("no token"));

Expand Down
6 changes: 6 additions & 0 deletions contracts/split/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ impl Invoice {
/// Upgrade a legacy (pre-version) invoice to the current schema.
/// New fields are filled with their default (empty / zero) values.
pub fn from_legacy(old: LegacyInvoice, env: &Env) -> Self {
let num_recipients = old.recipients.len();
let mut vesting_cliff_claimed = Vec::new(env);
for _ in 0..num_recipients {
vesting_cliff_claimed.push_back(false);
}

Invoice {
version: 2,
creator: old.creator,
Expand Down