Skip to content

veNOTE

fact Source contracts/src/governance/veNOTE.sol. veNOTE is a non-transferable voting-weight record in the Curve tradition: you lock NOTE for a fixed term and receive weight that falls linearly to zero at the unlock time. It implements IVotes in timestamp mode and is the IVotes source for both governors and for gauge votes.

Weight
amount × (end − t) / MAXTIME
MAXTIME
4 × 365 days
Minimum term
1 week
Delegation
none
  • end is the unlock time you choose, floored to a week boundary (Thursday 00:00 UTC). A lock ending in 4 years from today has a weight of about 1 veNOTE per NOTE; a 1-year lock starts at about 0.25.
  • Weight decays every second. Nothing decays your principal: the NOTE amount stays fixed and is returned in full by withdraw() once the lock expires.
  • One lock per address. There is no delegation: delegates(a) returns a, and delegate or delegateBySig revert DelegationNotSupported.
  • Weight is exact: balanceOfAt and totalSupplyAt use a binary search on checkpointed points, so a governor snapshot at any past timestamp reads the true value at that instant.
100k 75k 50k 25k 0 now 1 y 2 y 3 y 4 y Alice · 100,000 NOTE · 4 years · 100,000 ve Bob · 60,000 NOTE · 2 years · 30,000 ve dashed: 60,000 NOTE · 1 year · 15,000 ve
Illustrative. Weight in veNOTE (y) against time (x). Each lock is a straight line from amount × (end − now) / MAXTIME at creation to zero at its unlock time. Longer locks start higher and fall more slowly in relative terms.

illustrative Same locks as the worked weekly example.

AliceBob
Locked100,000 NOTE60,000 NOTE
Term4 years (max)2 years
Slope amount / MAXTIME100,000e18 / 126,144,000 ≈ 7.93e11 per second60,000e18 / 126,144,000 ≈ 4.76e11 per second
Weight at lock100,000 × 4/4 = 100,000 ve60,000 × 2/4 = 30,000 ve
Weight after 1 week100,000 − 7.93e11 × 604,800 / 1e18 ≈ 99,520 ve30,000 − 4.76e11 × 604,800 / 1e18 ≈ 29,712 ve
Weight after 1 year75,000 ve15,000 ve
Weight at unlock00
Share of total (130,000 at lock)76.9%23.1%

Bob’s lock loses 0.96% of its weight in the first week; Alice’s loses 0.48%. The shorter lock decays faster in relative terms, so relative weights drift toward longer lockers between votes unless Bob extends.

fact

FunctionWhoReverts
createLock(amount, unlockTime)anyone, whenNotPausedZeroAmount, LockExists, UnlockTimeInPast, UnlockTimeTooFar (more than 4 years)
increaseAmount(amount)the lockerNoLock, LockExpired
depositFor(account, amount)anyoneadds NOTE to another address’s lock; NoLock, LockExpired
increaseUnlockTime(unlockTime)the lockerUnlockTimeNotIncreased, UnlockTimeTooFar, LockExpired
lockFromStaked(shares, unlockTime)anyonepulls sNOTE shares and credits their convertToAssets value; unlockTime must be non-zero for a new lock and 0 for a top-up (UnlockTimeMustBeZero)
withdraw()the locker, never pausableNoLock, LockNotExpired; returns NOTE and any escrowed sNOTE shares. Succeeds even when the global history is more than 255 weeks behind (see below)
checkpoint()anyonefills the global point history; each call walks at most 255 weeks
balanceOf, balanceOfAt(account, ts), totalSupply, totalSupplyAt(ts)viewFutureLookup for ts > now
getVotes, getPastVotes, getPastTotalSupply, clock, CLOCK_MODEview (IVotes)getPast* need timepoint < clock()
locked, lockedEnd, getLastUserSlope, userPointHistory, pointHistory, userPointEpoch, epochview

Every mutating call re-checkpoints the user and the global supply, so totalSupply is never stale by more than the caller’s own change. The global walk covers at most 255 weeks per call; if the contract has been idle for longer than that, a lock, top-up or unlock-time extension reverts CheckpointRequired(reachedTs) until checkpoint() has been called enough times to bring the history up to the current block, so a non-zero user change is never merged into a stale global point.

withdraw() of an expired lock is the exception: its account delta is zero (the lock’s slope was already removed through slopeChanges[end] when the lock expired), so merging it into a partially walked global point changes nothing. The call persists the partial walk exactly as checkpoint() would and returns the funds; a later checkpoint() finishes the history and the global point sequence is identical whichever order the two calls run in. Withdrawal is therefore available at any time, as the pause rules promise.

fact Recovery cost after a long idle period. The scenario needs about five years with no lock, deposit, withdrawal or checkpoint on the contract. Measured in the test suite: after a 52 week lock and a 300 week idle period, one permissionless checkpoint() call walks 255 weeks for about 8.1M gas, and the next user action walks the remaining 45 weeks. Each further 255 idle weeks cost one more such call. Funds are delayed by at most one transaction, never lost, and the app decodes CheckpointRequired by name.

lockFromStaked does not redeem the vault shares (sNOTE withdrawals need requestWithdraw plus a cooldown). It escrows the shares inside veNOTE and credits the lock with their asset value at that moment. Voting weight is fixed at lock time and does not track later vault yield, which is the conservative choice; the shares, including any yield accrued while escrowed, come back with withdraw().

ConsumerReadsNote
NoteGovernorgetPastVotes(voter, snapshot), getPastTotalSupplySnapshot is proposal creation plus votingDelay (1 day). Proposal threshold is 0.5% of supply at clock() − 1.
UpgradeGovernorsameQuorum 8% of supply at snapshot.
GaugeControllergetLastUserSlope, lockedEndAllocates your slope across gauges; the lock must extend past the next week boundary (LockExpiresTooSoon).
  • No transfer, no wrapping, no delegation. Weight belongs to the address that locked.
  • No early exit. withdraw() reverts LockNotExpired until end.
  • Not a boost. sNOTE staking rewards are not multiplied by veNOTE in v1; bribe markets and lock boost are listed as future work in the contract documentation.
  • Pausable creation only. The guardian can pause createLock, increaseAmount, depositFor, increaseUnlockTime and lockFromStaked; withdraw always works.