Module 09: Vesting
Client: client.vesting
Weight: Light
Prerequisites
- Wallet funded with the main's pair asset (→02) and a token to vest
- Understand loan mechanics if you plan to take a vesting loan
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:
| Type | Behavior |
|---|---|
| Cliff | All tokens unlock at a single point in time, between 1 hour and 990 days from now |
| Gradual | Tokens stream linearly over a duration of whole days (1 to 990), unlocking at each time unit interval |
Roles. A wrong caller reverts.
| Call | Who | After renounceVesting | With an open loan |
|---|---|---|---|
addTokensToVesting | creator | still allowed | reverts "Loan open: repay first" |
changeBeneficiary | creator | reverts "Vesting renounced" | reverts with no reason |
extendVestingPeriod | creator or beneficiary | the creator reverts "Vesting renounced"; the beneficiary may still extend | reverts "Loan open: repay first" |
transferCreatorRole | creator | still allowed | still allowed |
renounceVesting | creator, once, cannot be undone | - | still allowed |
claimTokens | beneficiary | unchanged | reverts with no reason; getClaimableAmount reads 0 |
takeLoanOnVesting, repayExtendAndReborrow | beneficiary | unchanged | see §d |
repayLoanOnVesting | beneficiary, or the creator | the creator reverts "Only beneficiary or creator"; the beneficiary may still repay | see §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 7 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
)
| Param | Type | Notes |
|---|---|---|
beneficiary | string | Recipient address, not zero |
token | string | Token contract to vest |
totalAmount | bigint/int | Total tokens (18 decimals) |
startTime | bigint/int | Unix timestamp; a past value becomes now; at most 7 days ahead |
durationInDays | bigint/int | Total vesting duration in whole days, 1 to 990 |
timeUnit | number | Unlock frequency: 0=seconds, 1=minutes, 2=hours, 3=days |
memo | string | Optional label |
ecosystem | string | The 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") |
renounceOnCreation | bool | true 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.
| Param | Type | Notes |
|---|---|---|
beneficiary | string | Recipient address, not zero |
token | string | Token contract to vest |
totalAmount | bigint/int | Total tokens (18 decimals) |
unlockTime | bigint/int | Unix timestamp, now + 3600 to now + 990 days |
memo | string | Optional label |
ecosystem | string | The token's main (as above) |
renounceOnCreation | bool | true = 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
| Method | Description |
|---|---|
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.
| Method | Who | Notes |
|---|---|---|
changeBeneficiary(vestingId, newBeneficiary) | creator, unless renounced | Transfers recipient role to new address |
extendVestingPeriod(vestingId, additionalDays) | creator (unless renounced) or beneficiary | Adds 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) | creator | Adds tokens to the schedule; auto-approves |
transferCreatorRole(vestingId, newCreator) | creator | Hands off creator control |
renounceVesting(vestingId) | creator | Permanent: 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.
| Param | Type | Description |
|---|---|---|
vestingId | bigint/int | Vesting 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
| Method | Returns | Description |
|---|---|---|
getVestingDetails(vestingId) | Vesting struct | Full schedule details: parties, token, amounts, timing, loan status |
getClaimableAmount(vestingId) | bigint | Tokens available to claim right now; 0 while a loan is open |
getVestedAmount(vestingId) | bigint | Total tokens vested so far (including already claimed) |
effectiveTotalAmount(vestingId) | bigint | What the schedule holds after a liquidation, before it is settled |
effectiveVestedAmount(vestingId) | bigint | The vested amount on that effective total |
loanHubOf(vestingId) | address | The 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) | bigint | Active 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() | bigint | Total 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.
| Field | Type | Description |
|---|---|---|
creator | address | Who created the schedule |
beneficiary | address | Who receives tokens |
token | address | Vested token contract |
ecosystem | address | The token's main (the main the vesting's loans use) |
totalAmount | uint256 | Total tokens in schedule |
claimedAmount | uint256 | Tokens already claimed |
startTime | uint256 | Unix timestamp when vesting begins |
durationInDays | uint256 | Gradual duration (0 for cliff) |
unlockTime | uint256 | Cliff unlock timestamp (0 for gradual) |
isGradual | bool | true = gradual, false = cliff |
activeLoanId | uint256 | Active loan ID, 0 if none |
memo | string | Label set at creation |
timeUnit | uint8 | 0=seconds, 1=minutes, 2=hours, 3=days |
renounced | bool | true once the creator renounced |
Fees
| Action | Fee |
|---|---|
| Create vesting (any type) | feeAmount() in ETH, only when feeEnabled() is true (off by default) |
| Vesting loan origination | 2% of the collateral value |
| Vesting loan interest | 0.005% per day, prepaid |
Errors
| Error | When | Fix |
|---|---|---|
| "Vesting does not exist" | extendVestingPeriod(), addTokensToVesting() on an invalid ID | Verify 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 schedule | Call 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 caller | Call from the creator wallet |
| "Only creator or beneficiary can extend" | extendVestingPeriod() from another wallet | Call 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 string | changeBeneficiary(), claimTokens() while a loan is open | Repay (or settle) the loan first |
| "Loan not active" | repayExtendAndReborrow() after a liquidation | Settle 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 calls | Duration or unlockTime must be at least 3600 seconds from now |
| "Duration too long" | both create calls | More than 990 days |
| "End too far" | gradual create calls | Start plus duration ends more than 990 days from now |
| "Unlock time must be in the future" | createCliffVesting() | Use a future timestamp |
| "Start too far" | gradual create calls | startTime more than 7 days ahead (a millisecond timestamp, for one); pass seconds |
| "Invalid beneficiary address" | every create call | Pass a non-zero beneficiary |
| "Invalid ecosystem" / "Not an Ecosystem token" | both create calls | Pass the token's main as ecosystem, and a token of that main |
| "Insufficient fee" | create calls while the fee is on | Send 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()orbatchCreateCliffVesting()for investor rounds to save gas
See Also
- Module 05: Lending: Loan fees, extensions, repay flow in the main's pair asset, same mechanics apply to vesting loans
- Module 07: Token Creation: Launch a token, then vest allocations from it
- Module 10: Portfolio & Info: Query all your vesting schedules and claimable amounts
- Module 16: Trust & Security: Why locked tokens build platform trust
- Module 18: SDK Reference: Full vesting method signatures and errors