Module 09: Vesting

Client: client.vesting Weight: Light

Prerequisites

Next steps after vesting

  • Need liquidity before unlock? Take a vesting loan (§d below; it pledges every unclaimed token)
  • Launching with a distribution plan? Combine with token creation
  • Distributing to many recipients? Use batch creation (§a)

Why Vesting Matters

Vesting locks tokens and releases them on a schedule you define. Use it to:

  • Lock team/advisor/investor tokens to signal long-term commitment
  • Distribute allocations that release over months or years
  • Build trust with your community: locked tokens can't be dumped immediately
  • Access liquidity before unlock by taking a loan against a vesting position

How It Works

Two vesting types:

TypeBehavior
CliffAll tokens unlock at a single point in time, between 1 hour and 990 days from now
GradualTokens stream linearly over a duration of whole days (1 to 990), unlocking at each time unit interval

Roles. A wrong caller reverts.

CallWhoAfter renounceVestingWith an open loan
addTokensToVestingcreatorstill allowedreverts "Loan open: repay first"
changeBeneficiarycreatorreverts "Vesting renounced"reverts with no reason
extendVestingPeriodcreator or beneficiarythe creator reverts "Vesting renounced"; the beneficiary may still extendreverts "Loan open: repay first"
transferCreatorRolecreatorstill allowedstill allowed
renounceVestingcreator, once, cannot be undone-still allowed
claimTokensbeneficiaryunchangedreverts with no reason; getClaimableAmount reads 0
takeLoanOnVesting, repayExtendAndReborrowbeneficiaryunchangedsee §d
repayLoanOnVestingbeneficiary, or the creatorthe creator reverts "Only beneficiary or creator"; the beneficiary may still repaysee §d

The creator can give up changeBeneficiary, their own extendVestingPeriod and their own repayLoanOnVesting with renounceVesting(vestingId), or at creation with renounceOnCreation = true. Adding tokens and transferring the creator role still work after renouncing.

An open loan blocks four actions. extendVestingPeriod and addTokensToVesting revert "Loan open: repay first"; changeBeneficiary and claimTokens revert with no reason. It does not block transferCreatorRole or renounceVesting. Repay first, or use repayExtendAndReborrow(vestingId, additionalDays).

No price liquidation on vesting loans: expiry is time-based only, same as regular loans. After liquidationTime a keeper liquidates the position. Assume it can happen in the first block after the deadline. Until that transaction lands a repay or a paid extension still goes through; do not plan on the gap. After a liquidation the beneficiary (or the creator, unless the schedule is renounced) calls repayLoanOnVesting(vestingId), which pays nothing, collects what the liquidation left and shrinks the schedule to claimed + recovered. Do this within 365 days of the liquidation; after that the leftover may no longer be claimable.


Actions

a. Create Vesting

createGradualVesting(beneficiary, token, totalAmount, startTime, durationInDays, timeUnit, memo, ecosystem, renounceOnCreation) (payable)

Creates a linear vesting schedule. Tokens unlock progressively at each timeUnit interval throughout the duration.

A startTime in the past is moved to the block time; it does not revert. A startTime more than 990 days ahead reverts "Start too far". The beneficiary may not be the zero address ("Invalid beneficiary address").

// JS
const result = await client.vesting.createGradualVesting(
  "0xBeneficiary",
  "0xToken",
  parseUnits("10000", 18),
  Math.floor(Date.now() / 1000) + 60,  // a past value becomes now
  365,   // duration in days
  3,     // timeUnit: 3 = daily unlocks
  "Team allocation",
  MAINTOKEN,  // the token's main
  false       // renounceOnCreation
);
# Python
import time
result = client.vesting.create_gradual_vesting(
    "0xBeneficiary", "0xToken", 10000 * 10**18,
    int(time.time()) + 60, 365, 3, "Team allocation",
    MAINTOKEN,  # the token's main
    False       # renounce_on_creation
)
ParamTypeNotes
beneficiarystringRecipient address, not zero
tokenstringToken contract to vest
totalAmountbigint/intTotal tokens (18 decimals)
startTimebigint/intUnix timestamp; a past value becomes now; at most 990 days ahead
durationInDaysbigint/intTotal vesting duration in whole days, 1 to 990
timeUnitnumberUnlock frequency: 0=seconds, 1=minutes, 2=hours, 3=days
memostringOptional label
ecosystemstringThe token's main: token.quoteToken(), or the token itself when it is a main. It must be a main VESTING knows ("Invalid ecosystem"), and token must be that main or a token of its factory ("Not an Ecosystem token")
renounceOnCreationbooltrue renounces at creation: the creator can never change the beneficiary, extend or repay a vesting loan

Fee. The call is payable. Send feeAmount() of ETH only when feeEnabled() is true and feeWhitelist(you) is false; otherwise send 0. The fee is off by default. Anything sent above the fee is refunded.

timeUnit examples: durationInDays=30, timeUnit=2 = hourly unlocks over 30 days (720 events). durationInDays=30, timeUnit=3 = daily unlocks over 30 days (30 events).


createCliffVesting(beneficiary, token, totalAmount, unlockTime, memo, ecosystem, renounceOnCreation) (payable)

All tokens unlock at unlockTime in a single event. unlockTime must be 1 hour to 990 days from now: anything else reverts.

ParamTypeNotes
beneficiarystringRecipient address, not zero
tokenstringToken contract to vest
totalAmountbigint/intTotal tokens (18 decimals)
unlockTimebigint/intUnix timestamp, now + 3600 to now + 990 days
memostringOptional label
ecosystemstringThe token's main (as above)
renounceOnCreationbooltrue = the creator can never change the beneficiary, extend or repay a vesting loan

Fee. As for the gradual call: feeAmount() in ETH only while feeEnabled() is true and you are not fee-whitelisted (off by default); a surplus is refunded.


Batch Creation

MethodDescription
batchCreateGradualVesting(beneficiaries[], token, totalAmounts[], userMemos[], startTime, durationInDays, timeUnit, ecosystem, renounceOnCreation)Multiple gradual schedules in one transaction.
batchCreateCliffVesting(beneficiaries[], token, totalAmounts[], unlockTime, userMemos[], ecosystem, renounceOnCreation)Multiple cliff schedules in one transaction.

Only beneficiaries, amounts and memos are arrays, and they must have the same length ("Arrays length mismatch"). The memo array comes before startTime in the gradual call and after unlockTime in the cliff call. Both calls are payable, with one fee per batch.

Batch creation is the efficient path when distributing to many recipients at once (e.g., investor round, team allocation).


b. Claim Tokens

claimTokens(vestingId)

Claims all currently unlocked tokens. Only the beneficiary can call this.

  • For cliff vesting: only callable after unlockTime
  • For gradual vesting: callable anytime after vesting starts, claims whatever has vested since last claim
  • Check getClaimableAmount(vestingId) before calling to confirm non-zero amount (it reads 0 while a loan is open)
const claimable = await client.vesting.getClaimableAmount(vestingId);
if (claimable > 0n) {
  await client.vesting.claimTokens(vestingId);
}

c. Manage Vesting

Creator calls, except that the beneficiary may also extend.

MethodWhoNotes
changeBeneficiary(vestingId, newBeneficiary)creator, unless renouncedTransfers recipient role to new address
extendVestingPeriod(vestingId, additionalDays)creator (unless renounced) or beneficiaryAdds days to the vesting duration (the remaining time may not pass 990 days); fails if vesting already ended; reverts "Loan open: repay first" with a loan
addTokensToVesting(vestingId, additionalAmount)creatorAdds tokens to the schedule; auto-approves
transferCreatorRole(vestingId, newCreator)creatorHands off creator control
renounceVesting(vestingId)creatorPermanent: the creator can no longer change the beneficiary, extend or repay a vesting loan

Blocked by an open loan: changeBeneficiary, extendVestingPeriod and addTokensToVesting. Repay the vesting loan first, or use repayExtendAndReborrow (§d).


d. Vesting Loans (Beneficiary; the Creator May Repay)

Borrow against a vesting position to access liquidity before tokens unlock.

takeLoanOnVesting(vestingId)

Takes a loan in the pair asset of the vesting's main. You choose neither the amount nor the term: it pledges every unclaimed token (totalAmount − claimedAmount) for the rest of the schedule plus loanBuffer() days (10), and pays the beneficiary the pair asset of the token's main.

ParamTypeDescription
vestingIdbigint/intVesting schedule ID

Preview: the loan is sized at the collateral value of every unclaimed token (totalAmount − claimedAmount; quote it as in Module 05). A factory token is valued at its floor, not its market price. The term is the schedule's remaining whole days plus loanBuffer(). You receive that value less the origination fee (staticFeePercentage()) and the prepaid daily fee for the term (getDynamicFee(value, days)). A position worth less than minimumLoan() cannot borrow.

  • Origination fee: 2% of the collateral value (today's value; read staticFeePercentage() on the main)
  • Interest: 0.005% per day, prepaid for the whole term (today's value; read dynamicFeePercentage() on the main)
  • Duration: set by the contract (the schedule's remaining days plus loanBuffer())
  • Expiry: time-based only: no price liquidation
  • Reverts: "Active loan exists", "No unclaimed tokens"

repayLoanOnVesting(vestingId)

Beneficiary, or the creator while the schedule is not renounced. Pulls the loan's fullAmount of the main's pair asset from the caller into the vesting contract: approve VESTING (the vesting entry of /contracts.json), not the loan hub. The loan is read from the hub it was opened on (loanHubOf(vestingId)). The tokens go back into the schedule, not to the caller. When the creator repays, the beneficiary keeps what it borrowed. After a liquidation the same call settles instead of paying (see How It Works).

repayExtendAndReborrow(vestingId, additionalDays)

Beneficiary only. Repays the open loan in full, extends the schedule by additionalDays and opens a new loan, in one transaction. The new loan pays the origination fee again. It is the only way to lengthen a schedule that has a loan. Reverts "Loan not active" if the loan was liquidated (settle it with repayLoanOnVesting first). Approve the pair asset to VESTING for the repay, as above.

Only one active loan per vesting position at a time. Check getActiveLoan(vestingId) to see current loan ID.


Read Methods

MethodReturnsDescription
getVestingDetails(vestingId)Vesting structFull schedule details: parties, token, amounts, timing, loan status
getClaimableAmount(vestingId)bigintTokens available to claim right now; 0 while a loan is open
getVestedAmount(vestingId)bigintTotal tokens vested so far (including already claimed)
effectiveTotalAmount(vestingId)bigintWhat the schedule holds after a liquidation, before it is settled
effectiveVestedAmount(vestingId)bigintThe vested amount on that effective total
loanHubOf(vestingId)addressThe hub the (latest) loan was taken on
getVestingsByBeneficiary(address)bigint[]All vesting IDs where address is beneficiary
getVestingsByCreator(address)bigint[]All vesting IDs created by address
getActiveLoan(vestingId)bigintActive loan ID, or 0 if none
getTokenVestingIds(token, startIndex, endIndex)bigint[]Vesting IDs for a token within index range
getVestingDetailsBatch(vestingIds[])Vesting[]Details for multiple schedules in one call
getVestingCount()bigintTotal vesting schedules ever created (contract: vestingCount())

getVestingDetails struct fields (14, in contract order). Read the struct by field name. A positional reader written for the old 12-field list is off by one from index 3.

FieldTypeDescription
creatoraddressWho created the schedule
beneficiaryaddressWho receives tokens
tokenaddressVested token contract
ecosystemaddressThe token's main (the main the vesting's loans use)
totalAmountuint256Total tokens in schedule
claimedAmountuint256Tokens already claimed
startTimeuint256Unix timestamp when vesting begins
durationInDaysuint256Gradual duration (0 for cliff)
unlockTimeuint256Cliff unlock timestamp (0 for gradual)
isGradualbooltrue = gradual, false = cliff
activeLoanIduint256Active loan ID, 0 if none
memostringLabel set at creation
timeUnituint80=seconds, 1=minutes, 2=hours, 3=days
renouncedbooltrue once the creator renounced

Fees

ActionFee
Create vesting (any type)feeAmount() in ETH, only when feeEnabled() is true (off by default)
Vesting loan origination2% of the collateral value
Vesting loan interest0.005% per day, prepaid

Errors

ErrorWhenFix
"Vesting does not exist"extendVestingPeriod(), addTokensToVesting() on an invalid IDVerify via getVestingsByBeneficiary() or getVestingsByCreator()
"Only beneficiary can claim" / "Only beneficiary can take loan" / "Only beneficiary"Wrong caller (claimTokens, takeLoanOnVesting, repayExtendAndReborrow)Call from the beneficiary wallet
"Only beneficiary or creator"repayLoanOnVesting() from another wallet, or from the creator of a renounced scheduleCall from the beneficiary wallet (or the creator's, if not renounced)
"Only creator can change beneficiary" / "Only creator can add tokens" / "Only creator can renounce" / "Only creator can transfer role"Wrong callerCall from the creator wallet
"Only creator or beneficiary can extend"extendVestingPeriod() from another walletCall from the creator or beneficiary wallet
"Vesting renounced"changeBeneficiary(), the creator's extendVestingPeriod()The creator renounced; the beneficiary may still extend
"Already renounced"renounceVesting()Nothing to do
"No tokens to claim"claimTokens()Still in cliff period or all tokens claimed; check getClaimableAmount()
"No unclaimed tokens"takeLoanOnVesting()Nothing left to pledge
"Active loan exists"takeLoanOnVesting()One loan per vesting
"Loan open: repay first"extendVestingPeriod(), addTokensToVesting()Repay the vesting loan first, or use repayExtendAndReborrow()
no reason stringchangeBeneficiary(), claimTokens() while a loan is openRepay (or settle) the loan first
"Loan not active"repayExtendAndReborrow() after a liquidationSettle with repayLoanOnVesting()
"Vesting already ended"extendVestingPeriod(), addTokensToVesting()Can't change a completed schedule; create a new one
"Extension exceeds maximum duration"extendVestingPeriod()The remaining time may not pass 990 days
"Vesting duration must be at least 1 hour"both create callsDuration or unlockTime must be at least 3600 seconds from now
"Duration too long"both create callsMore than 990 days
"Unlock time must be in the future"createCliffVesting()Use a future timestamp
"Start too far"gradual create callsstartTime more than 990 days ahead (a millisecond timestamp, for one); pass seconds
"Invalid beneficiary address"every create callPass a non-zero beneficiary
"Invalid ecosystem" / "Not an Ecosystem token"both create callsPass the token's main as ecosystem, and a token of that main
"Insufficient fee"create calls while the fee is onSend at least feeAmount()

What Next

  • Monitor claims: Poll getClaimableAmount() periodically for beneficiaries to know when to claim
  • Loan against positions: Use takeLoanOnVesting() for liquidity before unlock (it pledges every unclaimed token)
  • Trust signal: Publicly visible locked tokens signal commitment; use alongside token launches
  • Batch distribution: Use batchCreateGradualVesting() or batchCreateCliffVesting() for investor rounds to save gas

See Also