Sylan Vesting
Owner‑managed, non‑upgradeable vesting contract for SYL. Supports cliff + linear vesting with slice periods, revocable schedules, and pull‑payment withdrawals. Uses Solady libraries (Ownable, ReentrancyGuard, SafeTransferLib) and a minimal ERC‑20 interface.
Timestamps in this contract are seconds (Solidity
block.timestamp), not milliseconds.
What it does
- Lock an amount of SYL for a beneficiary and release it linearly after an optional cliff.
- Allow the owner to revoke revocable schedules (releasing any vested portion first).
- Keep a running total of allocated tokens so the owner can only withdraw unallocated balances.
Data model
struct VestingSchedule {
address beneficiary; // who receives tokens
uint256 cliff; // timestamp = start + cliffSeconds
uint256 start; // vesting start (unix seconds)
uint256 duration; // total duration (seconds)
uint256 slicePeriodSeconds; // vest accrues in these step sizes (>=1)
bool revocable; // can owner revoke?
uint256 amountTotal; // total allocated to this schedule
uint256 released; // amount already claimed
bool revoked; // if true, no more vesting accrues
}- Schedule ID:
keccak256(abi.encodePacked(holder, index))
Helpers:computeNextVestingScheduleIdForHolder(holder),computeVestingScheduleIdForAddressAndIndex(holder, index).
Lifecycle
1) Create
createVestingSchedule(beneficiary, start, cliffSeconds, duration, slicePeriodSeconds, revocable, amount) — onlyOwner
Checks:
amount > 0,duration > 0,slicePeriodSeconds ≥ 1,duration ≥ cliffSeconds- Contract
getWithdrawableAmount() ≥ amount - Records schedule under a new scheduleId and increments the holder’s schedule count.
- Emits
VestingCreated(scheduleId, beneficiary, amount)
2) Release
release(scheduleId, amount) — callable by beneficiary or owner
- Computes releasable via
_computeReleasableAmount(see Math). - Transfers
amountSYL to beneficiary and bumpsreleased. - Emits
TokensReleased(scheduleId, beneficiary, amount)
3) Revoke (optional)
revoke(scheduleId) — onlyOwner
- Requires
revocable == trueand not already revoked. - First releases any currently vested amount, then marks the schedule revoked.
- Unreleased remainder is removed from
vestingSchedulesTotalAmount. - Emits
VestingRevoked(scheduleId, beneficiary, unreleasedAmount)
4) Withdraw unallocated
withdraw(amount) — onlyOwner
- Transfers out tokens that are not locked by active schedules.
- Guarded by
nonReentrantand checksamount ≤ getWithdrawableAmount(). - Emits
TokensWithdrawn(to, amount)
Math (releasable amount)
Let t = block.timestamp.
- If
t < clifforrevoked == true→ 0. - If
t ≥ start + duration→amountTotal - released(everything left). - Else linear with slicing:
timeFromCliff = t - cliffvestedSeconds = floor(timeFromCliff / slice) * slicevestedAmount = amountTotal * vestedSeconds / durationreleasable = max(vestedAmount - released, 0)
getCurrentTime()is an internalvirtualhelper returningblock.timestamp(useful for testing via inheritance/mocks).
External interface (selected)
Writes
createVestingSchedule(address,uint256,uint256,uint256,uint256,bool,uint256)release(bytes32,uint256)revoke(bytes32)withdraw(uint256)
Reads
getVestingSchedule(bytes32) → VestingSchedulecomputeReleasableAmount(bytes32) → uint256getReleasableAmountForBeneficiary(address) → uint256(sums across that address)getVestingSchedulesCountByBeneficiary(address) → uint256getVestingSchedulesCount() → uint256getVestingSchedulesTotalAmount() → uint256getVestingScheduleByAddressAndIndex(address,uint256) → VestingSchedulegetVestingIdAtIndex(uint256) → bytes32getToken() → addressgetWithdrawableAmount() → uint256computeNextVestingScheduleIdForHolder(address) → bytes32computeVestingScheduleIdForAddressAndIndex(address,uint256) → bytes32
Receive/Fallback
receive()andfallback()are present (no-op) to avoid accidental reverts on ETH transfers.
Events
VestingCreated(bytes32 scheduleId, address beneficiary, uint256 amount)TokensReleased(bytes32 scheduleId, address beneficiary, uint256 amount)VestingRevoked(bytes32 scheduleId, address beneficiary, uint256 unreleasedAmount)TokensWithdrawn(address to, uint256 amount)
Minimal ABI (client claims)
For UIs that only let a beneficiary claim and inspect schedules:
[
{"type":"function","stateMutability":"view","name":"getVestingSchedulesCountByBeneficiary","inputs":[{"name":"beneficiary","type":"address"}],"outputs":[{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"getVestingScheduleByAddressAndIndex","inputs":[{"name":"holder","type":"address"},{"name":"index","type":"uint256"}],"outputs":[{"type":"tuple","components":[
{"name":"beneficiary","type":"address"},
{"name":"cliff","type":"uint256"},
{"name":"start","type":"uint256"},
{"name":"duration","type":"uint256"},
{"name":"slicePeriodSeconds","type":"uint256"},
{"name":"revocable","type":"bool"},
{"name":"amountTotal","type":"uint256"},
{"name":"released","type":"uint256"},
{"name":"revoked","type":"bool"}
]}]},
{"type":"function","stateMutability":"view","name":"computeReleasableAmount","inputs":[{"name":"scheduleId","type":"bytes32"}],"outputs":[{"type":"uint256"}]},
{"type":"function","stateMutability":"nonpayable","name":"release","inputs":[{"name":"scheduleId","type":"bytes32"},{"name":"amount","type":"uint256"}],"outputs":[]}
]Integration tips
- Decimals: token uses 18 decimals; format UI amounts accordingly.
- Claim UX: display
computeReleasableAmount(id)and allow partial or full release. - Safety: wrap
releasein a try/catch or pre‑read the releasable amount to avoid spurious reverts. - Admin UI: hide revoke unless
revocable == true; warn that revoke is final and only frees the unreleased portion.
Admin & security notes
- Only the owner can create schedules, revoke, and withdraw unallocated tokens.
withdraw(amount)cannot reduce reserved funds below active allocations (vestingSchedulesTotalAmount).- Consider holding ownership in a Gnosis Safe; keep the vesting contract funded before creating schedules.
Verification checklist
- Schedules created for correct beneficiaries/amounts (unit tests).
- Cliff/duration/slice parameters reviewed;
duration ≥ cliff. -
getWithdrawableAmount()always ≥ 0; treasury top‑ups tested. - Revoke path tested releases vested portion first.
-
getReleasableAmountForBeneficiarymatches sum of individual schedules.
Out of scope for this page
- Token roles & upgrades → Sylan Token page.
- Addresses/ABIs → Architecture → Addresses & ABIs.
- Presale/vesting allocations → see Presale and project tokenomics docs.