Vesting schedules allow vault funds to be released to beneficiaries in equal installments over time, rather than as a lump sum on trigger_release.
- Owner attaches a schedule (while vault is
Locked) viaset_vesting_schedule. - Vault expires and anyone calls
trigger_release. Because a schedule is attached, the vault transitions toReleasedbut the balance stays in the contract. - Beneficiary claims available installments via
claim_vested_installment. Each call transfers all unlocked-but-unclaimed tranches.
// 1. Create vault and deposit 1,000 XLM
let vault_id = client.create_vault(&owner, &beneficiary, &check_in_interval);
client.deposit(&vault_id, &owner, &1_000_000_000); // 100 XLM in stroops
// 2. Attach vesting schedule with a 1-year cliff
// start_time: Unix timestamp of first claimable installment
// interval: seconds between installments (1 year ≈ 31_536_000 s)
// num_installments: 4
// cliff_period: seconds after start_time before any claim is allowed (1 year)
client.set_vesting_schedule(
&vault_id,
&owner,
&start_time, // e.g. env.ledger().timestamp() + 31_536_000
&31_536_000u64, // 1 year in seconds
&4u32,
&31_536_000u64, // 1-year cliff
);
// 3. After vault expires, anyone triggers release (no funds move yet)
client.trigger_release(&vault_id);
// 4. Beneficiary claims each year (first claim only possible after cliff)
client.claim_vested_installment(&vault_id); // year 1: 250 XLM
// ... one year later ...
client.claim_vested_installment(&vault_id); // year 2: 250 XLMfn set_vesting_schedule(
env: Env,
vault_id: u64,
caller: Address, // must be vault owner
start_time: u64, // Unix timestamp of first claimable installment
interval: u64, // seconds between installments (must be > 0)
num_installments: u32 // total number of tranches (must be > 0)
cliff_period: u64, // seconds after start_time before any claim is allowed (0 = no cliff)
) -> Result<(), ContractError>Constraints:
- Caller must be the vault owner.
- Vault must be
Locked(not yet released or cancelled). intervalandnum_installmentsmust both be > 0.- Vault balance must be > 0.
- Replaces any previously set schedule (claimed_installments resets to 0).
cliff_periodmay be 0 (disables cliff enforcement).
fn get_vesting_schedule(env: Env, vault_id: u64) -> Option<VestingSchedule>Returns the attached schedule, or None if none exists.
fn claim_vested_installment(env: Env, vault_id: u64) -> Result<i128, ContractError>Claims all installments that have become available since the last claim. Returns the total amount transferred.
Constraints:
- Vault must be
Released. - A vesting schedule must be attached.
- At least one new installment window must have elapsed since
start_time. - All installments must not already be claimed.
Errors:
| Code | Name | Meaning |
|---|---|---|
| 22 | VestingNotFound |
No schedule attached to this vault |
| 23 | NothingToClaimYet |
No new installments available (before start_time or between windows) |
| 24 | VestingAlreadyComplete |
All installments have been claimed |
| 55 | CliffNotReached |
Current time is before start_time + cliff_period |
A cliff period prevents any installment from being claimed until a minimum duration has elapsed since start_time. This is useful for enforcing a lock-up before vesting begins.
- Set
cliff_period > 0inset_vesting_scheduleto enable. - Set
cliff_period = 0to disable (default behaviour, no lock-up). - Attempting to claim before
start_time + cliff_periodreturnsCliffNotReached(error 55). - A
clif_rchevent is emitted on the first successful claim after the cliff (only once per schedule).
// Cliff of 1 year, then 4 quarterly installments
client.set_vesting_schedule(
&vault_id, &owner,
&start_time,
&7_884_000u64, // ~91 days per installment
&4u32,
&31_536_000u64, // 1-year cliff
);Each installment = total_amount / num_installments (integer division).
The last installment absorbs any remainder to ensure the full balance is distributed.
Example with 1,000 stroops over 3 installments:
- Installment 1: 333
- Installment 2: 333
- Installment 3: 334 (absorbs remainder)
Vesting is compatible with set_beneficiaries. Each claim distributes the installment amount proportionally by BPS, with the last beneficiary absorbing dust.
// Set 60/40 split
client.set_beneficiaries(&vault_id, &owner, &entries);
// Attach vesting schedule
client.set_vesting_schedule(&vault_id, &owner, &start, &interval, &4u32);
// Each claim splits the installment: 60% to ben_a, 40% to ben_b
client.claim_vested_installment(&vault_id);The vault owner may configure a reversal grace period on a per-claim basis. Instead of calling claim_vested_installment (which transfers immediately), use the 2-phase flow:
-
initiate_vesting_claim(vault_id, reversal_window_seconds)— calculates the claimable amount and holds it in escrow inside the contract. The schedule counter is advanced to block double-initiation. Returns the escrowed amount. -
reverse_vesting_claim(vault_id, caller)(owner only) — cancels the pending claim within the reversal window. The schedule counter is rolled back so the installments become claimable again. -
finalize_vesting_claim(vault_id)(anyone) — after the reversal window closes, completes the token transfer to the beneficiary.
fn initiate_vesting_claim(env: Env, vault_id: u64, reversal_window_seconds: u64) -> Result<i128, ContractError>
fn reverse_vesting_claim(env: Env, vault_id: u64, caller: Address) -> Result<(), ContractError>
fn finalize_vesting_claim(env: Env, vault_id: u64) -> Result<i128, ContractError>
fn get_pending_vesting_claim(env: Env, vault_id: u64) -> Option<VestingPendingClaim>| Error | Code | Meaning |
|---|---|---|
VestingReversalNotFound |
61 | No pending claim on this vault |
VestingReversalExpired |
60 | Reversal window has already closed |
InvalidAmount |
5 | A pending claim already exists (call finalize first) |
When enabled, any installments that were available but not claimed in a previous period roll over and accumulate. This ensures that a beneficiary doesn't lose out if they miss a claim window.
fn set_vesting_rollover(env: Env, vault_id: u64, caller: Address, enabled: bool) -> Result<(), ContractError>If a beneficiary declines their role, all remaining unvested funds are automatically transferred to a designated forfeiture recipient instead of remaining in the vault or returning to the owner.
fn set_vesting_forfeiture(env: Env, vault_id: u64, caller: Address, forfeiture_recipient: Address) -> Result<(), ContractError>Allows a designated oracle to immediately unlock all remaining vesting installments. This is typically used to handle the owner's passing.
fn set_vesting_acceleration(env: Env, vault_id: u64, caller: Address, oracle: Address) -> Result<(), ContractError>fn accelerate_vesting(env: Env, vault_id: u64, caller: Address) -> Result<(), ContractError>callermust be the designated oracle.
Stagger vesting across multiple beneficiaries with different schedules. Each beneficiary has their own start time, interval, and number of installments.
fn set_vesting_stagger(env: Env, vault_id: u64, caller: Address, entries: Vec<VestingStaggerEntry>) -> Result<(), ContractError>fn claim_staggered_vesting(env: Env, vault_id: u64, caller: Address) -> Result<i128, ContractError>When enabled, a beneficiary who missed claiming periods can catch up and claim all accumulated missed installments in a single call, rather than having to call claim_vested_installment multiple times.
fn set_vesting_catchup(
env: Env,
vault_id: u64,
caller: Address, // must be vault owner
enabled: bool,
max_catchup_installments: u32, // 0 = unlimited
) -> Result<(), ContractError>fn get_vesting_catchup(env: Env, vault_id: u64) -> Option<VestingCatchUpConfig>fn claim_catchup_vesting(env: Env, vault_id: u64) -> Result<i128, ContractError>Claims all unlocked-but-unclaimed installments at once. If max_catchup_installments is set, at most that many missed installments are claimed per call.
| Error | Code | Meaning |
|---|---|---|
CatchUpNotEnabled |
72 | Catch-up not configured or disabled for this vault |
VestingNotFound |
22 | No vesting schedule attached |
NothingToClaimYet |
23 | No missed installments to catch up on |
VestingAlreadyComplete |
24 | All installments already claimed |
| Topic | Data | Emitted when |
|---|---|---|
vest_cu |
(enabled, max_catchup_installments) |
Catch-up config set |
vest_cuc |
(beneficiary, amount, installments_claimed) |
Catch-up claimed |
Awards a bonus to the beneficiary when they claim on time — i.e., within on_time_window_seconds of an installment unlocking. Use claim_with_bonus instead of claim_vested_installment to receive the bonus.
fn set_vesting_bonus(
env: Env,
vault_id: u64,
caller: Address, // must be vault owner
bonus_bps: u32, // 1–10000 (e.g. 100 = 1%)
on_time_window_seconds: u64, // seconds after unlock to qualify
) -> Result<(), ContractError>fn get_vesting_bonus(env: Env, vault_id: u64) -> Option<VestingBonusConfig>fn claim_with_bonus(env: Env, vault_id: u64) -> Result<i128, ContractError>Claims available installments. If all claimable installments are within the on-time window, a bonus of bonus_bps / 10_000 is added on top of the base payout. If the vault balance cannot cover the bonus, the base amount is paid without bonus.
Example: 1% bonus, 7-day window, installment of 100 XLM claimed on time → 101 XLM paid.
| Error | Code | Meaning |
|---|---|---|
BonusNotEnabled |
73 | No bonus config set for this vault |
VestingNotFound |
22 | No vesting schedule attached |
NothingToClaimYet |
23 | No installments available |
VestingAlreadyComplete |
24 | All installments already claimed |
| Topic | Data | Emitted when |
|---|---|---|
vest_bon |
(bonus_bps, on_time_window_seconds) |
Bonus config set |
vest_bonc |
(beneficiary, total_amount, bonus_amount) |
Bonus claim completed |
...
The vault owner may attach a penalty config to a vesting schedule. If a beneficiary claims an installment more than grace_period_seconds after it unlocked, the payout for that installment is reduced by penalty_bps basis points.
fn set_vesting_penalty(
env: Env,
vault_id: u64,
caller: Address, // must be vault owner
penalty_bps: u32, // 1–10000 (e.g. 500 = 5%)
grace_period_seconds: u64, // seconds after unlock before penalty applies
) -> Result<(), ContractError>fn get_vesting_penalty(env: Env, vault_id: u64) -> Option<VestingPenaltyConfig>For each claimable installment:
- If
now > unlock_time + grace_period_seconds, the installment is "late". - Late installment payout =
per_installment * (1 - penalty_bps / 10_000). - On-time installments receive the full
per_installmentamount.
Example: 5% penalty, no grace period, 3 late installments of 100 each:
- Full payout would be 300.
- Penalized payout: 300 - 15 = 285.
| Topic | Data | Emitted when |
|---|---|---|
set_vest |
(start_time, interval, num_installments, total_amount, cliff_period) |
Schedule attached |
clm_vest |
(beneficiary, amount, installments_unlocked) |
Installment claimed (one event per beneficiary) |
clif_rch |
(timestamp,) |
First claim after cliff period is reached (emitted once per schedule) |