Functions¶
Note
VeRAACToken holds multiple lock positions per user, each keyed by its epoch-aligned unlockTime. Almost every function that touches a position therefore takes an epochEnd argument identifying which position to act on. Use getLocks to enumerate a user's active positions before calling increase, extend or ragequitLock.
Ragequit cooldown blocks most of this page
While a RagequitRequest is open for the caller, the notInRagequitCooldown modifier makes lock, increase, extend, withdraw and claimReward revert with PendingRagequitRequest(). The account is frozen until finalizeRagequit is called after the 7-day cooldown.
Locking¶
lock¶
lock(uint256 _amount, uint40 _epochs)
Summary
Transfers _amount RAAC from the caller and opens (or tops up) a lock position expiring _epochs epochs from now. The unlock time is rounded up to the next week boundary and clamped so the lock never exceeds maxTime. veRAAC is minted to match the resulting voting power.
If the caller had no prior locks, their reward cursor for every registered reward token is advanced to the current number of distributions — past distributions become permanently unclaimable, since the caller had no voting power at those snapshots.
Guarded Method
Callable by any address. Subject to whenNotPaused, nonReentrant and notInRagequitCooldown.
Parameters
| Name | Type | Description |
|---|---|---|
_amount |
uint256 | RAAC to lock; must be >= minLockAmount |
_epochs |
uint40 | Number of 7-day epochs; must be in [1, maxLockEpochs] |
Emits
Locked(address indexed user, uint256 amount, uint40 epochEnd)
Reverts
| Error | Condition |
|---|---|
AmountBelowMinimum() |
_amount < minLockAmount |
InvalidLockDuration() |
_epochs == 0 or _epochs > maxLockEpochs |
PendingRagequitRequest() |
caller has an open ragequit request |
ValueTooLargeToCast() |
resulting bias exceeds int128 |
Requirements
- Caller must have approved
_amountRAAC to the veRAAC contract - Locking into a week that already holds a position tops up that position rather than creating a second one
Typescript / ethers
Source code
function lock(uint256 _amount, uint40 _epochs) external nonReentrant whenNotPaused notInRagequitCooldown {
if (_amount < minLockAmount) revert AmountBelowMinimum();
if (_epochs == 0 || _epochs > _lockState.maxLockEpochs) {
revert InvalidLockDuration();
}
// Transfer tokens in
raac.safeTransferFrom(msg.sender, address(this), _amount);
IveRAACToken.LockedBalance[] memory locks = _lockState.userLockState[msg.sender].locks;
// Create lock via library
(int128 uBias, int128 gBias, uint40 epochEnd) = _lockState.createLock(msg.sender, _amount, _epochs);
address[] memory rT = rewardTokens;
if (locks.length == 0){
for (uint256 i = 0; i < rT.length; i++) {
address _rewardToken = rT[i];
RewardData storage rData = rewardData[_rewardToken];
userRewardData[msg.sender][_rewardToken].lastProcessedDistributionIndex = uint32(rData.distributions.length);
}
}
// Reconcile minted balance with actual voting power
_syncBalance(msg.sender, uBias);
emit Locked(msg.sender, _amount, epochEnd);
_afterLockUpdate(msg.sender, uBias, gBias);
}
increase¶
increase(uint256 _amountToAdd, uint40 _epochEnd)
Summary Adds RAAC to an existing position without changing its unlock time. The added amount earns voting power only for the position's remaining duration, so a top-up close to expiry mints very little power.
Guarded Method
Callable by any address holding a position at _epochEnd. Subject to whenNotPaused, nonReentrant and notInRagequitCooldown.
Parameters
| Name | Type | Description |
|---|---|---|
_amountToAdd |
uint256 | RAAC to add; must be >= minIncreaseAmount |
_epochEnd |
uint40 | Unlock timestamp of the position to top up; must be in the future |
Emits
LockIncreased(address indexed user, uint256 additionalAmount)
Reverts
| Error | Condition |
|---|---|
AmountBelowMinimum() |
_amountToAdd < minIncreaseAmount |
InvalidEpoch() |
_epochEnd <= block.timestamp |
MissingEpoch() |
caller has no active position at _epochEnd |
PendingRagequitRequest() |
caller has an open ragequit request |
Typescript / ethers
Source code
function increase(uint256 _amountToAdd, uint40 _epochEnd)
external nonReentrant whenNotPaused notInRagequitCooldown
{
if (_amountToAdd < minIncreaseAmount) revert AmountBelowMinimum();
if (_epochEnd <= block.timestamp) revert InvalidEpoch();
// Enforce per-position maximum for the targeted epoch end
uint256 currentAtEpoch = _userUnlockableLockAmountAt(msg.sender, _epochEnd);
if (currentAtEpoch == 0) revert LockManager.MissingEpoch();
// Transfer first
raac.safeTransferFrom(msg.sender, address(this), _amountToAdd);
// Update lock via library
(int128 uBias, int128 gBias) = _lockState.increaseLock(msg.sender, _amountToAdd, _epochEnd);
_syncBalance(msg.sender, uBias);
emit LockIncreased(msg.sender, _amountToAdd);
_afterLockUpdate(msg.sender, uBias, gBias);
}
extend¶
extend(uint40 _fromEpochEnd, uint40 _addEpochs)
Summary
Moves an entire position from _fromEpochEnd to _fromEpochEnd + _addEpochs × 7 days, clamped so the new end is never more than maxTime from now (rounded down to a week boundary when clamped). The locked amount is unchanged; voting power increases in proportion to the added duration.
If a position already exists at the new epoch end, the two are merged.
Guarded Method
Callable by any address holding a position at _fromEpochEnd. Subject to whenNotPaused, nonReentrant and notInRagequitCooldown.
Parameters
| Name | Type | Description |
|---|---|---|
_fromEpochEnd |
uint40 | Unlock timestamp of the position to extend |
_addEpochs |
uint40 | Epochs to add; must be in [1, maxLockEpochs] |
Emits
LockExtended(address indexed user, uint256 fromEpoch, uint256 newEpochEnd, uint256 amount)
Reverts
| Error | Condition |
|---|---|
InvalidEpoch() |
_addEpochs == 0 or > maxLockEpochs |
MissingEpoch() |
caller has no active position at _fromEpochEnd |
LockCannotBeExtended() |
the clamp produced no change, or either end is already in the past |
PendingRagequitRequest() |
caller has an open ragequit request |
Extending an already-maximal lock reverts
If the position already ends at (or within one week of) now + maxTime, the clamp resolves newEpochEnd to the same week as _fromEpochEnd and the call reverts with LockCannotBeExtended(). This is the normal outcome for a freshly created 52-epoch lock.
withdraw¶
withdraw() → uint256 withdrawn
Summary Withdraws the principal of every expired position the caller holds and transfers it in one RAAC transfer. Positions that have not yet expired are untouched.
Guarded Method
Callable by any address. Subject to nonReentrant and notInRagequitCooldown. Deliberately not whenNotPaused — see Design Trade-offs.
Returns
| Name | Type | Description |
|---|---|---|
withdrawn |
uint256 | Total RAAC transferred to the caller |
Emits
Withdrawn(address indexed user, uint256 amount)
Reverts
| Error | Condition |
|---|---|
LockNotExpired() |
no expired position with a non-zero amount |
NoTokensLocked() |
caller has never locked |
PendingRagequitRequest() |
caller has an open ragequit request |
No checkpoint is written
withdraw does not call _syncBalance and writes no checkpoint. Expired principal has already stopped contributing to the bias at its epoch boundary, so the caller's voting power is already correct; the stale rawBalanceOf is re-synced on their next state-changing call. See Design Trade-offs.
Early Exit (Ragequit)¶
ragequitLock¶
ragequitLock(uint40 _epochEnd)
Summary Initiates an early exit from a single position. The position must be the caller's earliest-expiring active lock. A fixed 5 % fee is burned; a time-weighted variable fee of up to 50 % is split 33 % to the Treasury and 67 % streamed to the remaining veRAAC holders. The caller's voting power for that position is zeroed immediately and the net RAAC is escrowed for 7 days.
Guarded Method
Callable by any address with an active, unexpired position. Subject to whenNotPaused and nonReentrant.
Parameters
| Name | Type | Description |
|---|---|---|
_epochEnd |
uint40 | Unlock timestamp of the position; must equal getMinLockEnd(caller) |
Emits
RagequitLockInitiated(address indexed user, uint40 indexed epochEnd, uint256 amount, uint256 fixedFee, uint256 variableFee, uint256 readyAt)RewardRedirectedToTreasury(address indexed token, uint256 amount)— for the Treasury leg, and for the holder leg when no eligible voting power remainsRewardNotified(address indexed token, uint256 reward, uint256 distributionLength)— when the holder leg becomes a distribution
Reverts
| Error | Condition |
|---|---|
PendingRagequitRequest() |
a request is already open |
NoTokensLocked() |
caller holds nothing |
InvalidEpoch() |
_epochEnd is in the past, or is not the caller's minimum lock end |
InvalidAmount() |
the position is empty |
Why only the earliest lock
The lock array is not sorted by unlock time, and the withdrawal cursor (nextUnlockIndex) advances monotonically. Restricting the exit to the minimum epoch end keeps the cursor consistent — see Design Trade-offs.
ragequitAll¶
ragequitAll() → (uint256 processed, uint256 amountProcessed)
Summary
Initiates an early exit from every active position at once. Fees are computed per position against each position's own remaining time, then summed. Behaves identically to ragequitLock from there on.
Guarded Method
Callable by any address with at least one active, unexpired position. Subject to whenNotPaused and nonReentrant.
Returns
| Name | Type | Description |
|---|---|---|
processed |
uint256 | Number of positions closed |
amountProcessed |
uint256 | Gross RAAC removed from locks, before fees |
Emits
RagequitInitiatedAll(address indexed user, uint256 grossAmount, uint256 penaltyFixed, uint256 penaltyVariable, uint256 readyAt)- plus the same reward events as
ragequitLock
Reverts
| Error | Condition |
|---|---|
PendingRagequitRequest() |
a request is already open |
InvalidEpoch() |
the caller has no unexpired position |
finalizeRagequit¶
finalizeRagequit(address _to)
Summary
Settles an initiated ragequit once the 7-day cooldown has elapsed. Deletes the request, recomputes the caller's voting power from any positions that survived the exit, and transfers the escrowed net RAAC to _to.
Guarded Method
Callable by the account that initiated the ragequit. Subject to whenNotPaused and nonReentrant.
Parameters
| Name | Type | Description |
|---|---|---|
_to |
address | Recipient of the net RAAC; cannot be the zero address |
Emits
RagequitFinalized(address indexed user, address indexed to, uint256 netAmount)
Reverts
| Error | Condition |
|---|---|
InvalidAddress() |
_to == address(0) |
NotFound() |
no open request |
CooldownNotElapsed() |
block.timestamp < readyAt |
Paused means stuck
Unlike withdraw, this function is gated by whenNotPaused, because it writes checkpoints that mutate global voting power. A pause during a cooldown delays settlement. This is intentional — see Design Trade-offs.
Rewards¶
claimReward¶
claimReward(address _token, uint256 maxDistributions)
Summary
Processes up to maxDistributions unclaimed distributions of _token for the caller, starting at their cursor, and transfers everything vested so far. Each distribution releases one epochs-th of the caller's share per elapsed week.
The cursor (lastProcessedDistributionIndex) advances only across the leading run of fully settled distributions, so a partially vested distribution keeps the cursor in place and is revisited on the next call.
Guarded Method
Callable by any address. Subject to whenNotPaused, nonReentrant and notInRagequitCooldown.
Parameters
| Name | Type | Description |
|---|---|---|
_token |
address | Registered reward token |
maxDistributions |
uint256 | Pagination bound; use getMaxUnclaimedDistributions to size it |
Emits
RewardPaid(address indexed token, address indexed user, uint256 reward, uint256 index)— once per distribution paid
Reverts
| Error | Condition |
|---|---|
TokenNotFound() |
_token is not a registered reward token |
InvalidAmount() |
the cursor is already at the end of the distribution array |
PendingRagequitRequest() |
caller has an open ragequit request |
distributeRewards¶
distributeRewards(address _rewardToken, uint256 _reward, uint40 epochs)
Summary
Pulls _reward of _rewardToken from the caller and opens a distribution that vests over epochs weekly epochs. Total voting power is snapshotted at block.number - 1, so a lock created in the same block cannot capture the distribution.
If the accumulated amount is still below minRewardDistributionAmount (18-decimal normalised), it is held in pendingRewardAmount and no distribution is created. If total voting power at the snapshot is zero, the full amount is sent to the Treasury.
Guarded Method
Requires REWARD_DISTRIBUTOR_ROLE. Subject to whenNotPaused and nonReentrant.
Parameters
| Name | Type | Description |
|---|---|---|
_rewardToken |
address | Registered reward token |
_reward |
uint256 | Amount in the token's native decimals |
epochs |
uint40 | Number of weekly epochs to vest over; must be non-zero |
Emits
RewardNotified(address indexed token, uint256 reward, uint256 distributionLength)RewardAccumulated(address indexed token, uint256 pendingAmount)— when below thresholdRewardRedirectedToTreasury(address indexed token, uint256 amount)— when total voting power is zero
Reverts
| Error | Condition |
|---|---|
TokenNotFound() |
token not registered |
InvalidAmount() |
_reward == 0, or nothing was actually received |
InvalidParams() |
epochs == 0 |
Fee-on-transfer safe
The amount credited is the measured balance delta, not the requested _reward.
Reward Token Administration¶
addRewardToken¶
addRewardToken(address _rewardToken)
Summary
Registers a new reward token. The token's decimals() is read and must be ≤ 18.
Guarded Method — onlyOwner.
Emits — RewardTokenAdded(address indexed token, address addedBy)
Reverts
| Error | Condition |
|---|---|
InvalidAddress() |
zero address |
TokenAlreadyAdded() |
already registered |
TokenPreviouslyRemoved() |
this token's removal was previously finalised |
InvalidParameterValue() |
decimals() > 18 |
A finalised removal is permanent
finalizeRemoveRewardToken leaves removalInitiatedAt non-zero, and this function rejects any token whose removalInitiatedAt != 0. Once a token has been fully removed it can never be registered again. Cancelling a pending removal clears the field, so an aborted removal is harmless.
initiateRemoveRewardToken¶
initiateRemoveRewardToken(address _rewardToken)
Summary
Toggles the removal cooldown for a reward token. First call starts a REWARD_REMOVAL_COOLDOWN (14 days) window, giving users time to claim; calling it again while a removal is pending cancels the removal.
Guarded Method — onlyOwner.
Emits
RewardTokenRemovalInitiated(address indexed token, uint256 removalAvailableAt)RewardTokenRemovalCanceled(address indexed token)
Reverts — TokenNotFound() if the token is not registered.
finalizeRemoveRewardToken¶
finalizeRemoveRewardToken(address _rewardToken)
Summary
Completes a removal after the cooldown. Everything still unclaimed (totalDistributed - totalClaimed) plus any accumulated pendingRewardAmount is swept to the Treasury, the token is de-registered and removed from rewardTokens by swap-and-pop.
Guarded Method — onlyOwner.
Emits
RewardRedirectedToTreasury(address indexed token, uint256 amount)RewardTokenRemoved(address indexed token)
Reverts
| Error | Condition |
|---|---|
TokenNotFound() |
token not registered |
RemovalNotInitiated() |
no pending removal |
CooldownNotElapsed() |
cooldown has not expired |
Users lose unclaimed rewards, and the token is gone for good
The sweep is unconditional — it does not check whether individual users still had claimable balances. The 14-day cooldown is the only protection. removalInitiatedAt is left set, so the token can never be re-registered, and rewardTokens ordering changes on removal (swap-and-pop), so off-chain indexers must not cache array indices.
Administration¶
| Function | Gate | Effect | Validation |
|---|---|---|---|
pause() / unpause() |
onlyOwner |
halts state-changing entry points except withdraw |
— |
setTreasuryAddress(address) |
onlyOwner |
fee and redirect destination | non-zero |
setTimePerBlock(uint256) |
onlyOwner |
block→seconds estimate for block-indexed views | non-zero |
setMinLockAmount(uint256) |
onlyOwner |
floor for lock |
>= 52 |
setMinIncreaseAmount(uint256) |
onlyOwner |
floor for increase |
>= maxTime |
setMinRewardDistributionAmount(uint256) |
onlyOwner |
18-dec distribution threshold | >= 1e18 |
setRewardTokenRemovalCooldown(uint256) |
onlyOwner |
removal cooldown | non-zero |
setRagequitEpochs(uint40) |
onlyOwner |
epochs the ragequit holder leg streams over | non-zero |
Each setter emits its corresponding ...Updated event carrying the old and new value.
Why the two lock floors differ
setMinLockAmount enforces >= 52 and setMinIncreaseAmount enforces >= maxTime. Both exist to keep a non-zero locked amount from carrying zero voting power: lock is always at least one epoch long, so 52 suffices, while increase can target a position one second from expiry and needs the full maxTime. See Parameters.
Views¶
Voting power¶
| Function | Returns | Notes |
|---|---|---|
balanceOf(user) |
uint256 | live decayed voting power — this is the number to integrate against |
totalSupply() |
uint256 | live global voting power |
balanceOfAtTime(user, ts) |
uint256 | exact historical power; reverts if ts >= block.timestamp |
totalSupplyAtTime(ts) |
uint256 | exact historical total; reverts if ts >= block.timestamp |
balanceOfAt(user, blockNumber) |
uint256 | estimated via timePerBlock; reverts if blockNumber >= block.number |
totalSupplyAt(blockNumber) |
(uint256, uint256) | estimated total and the timestamp it decayed to |
rawBalanceOf(user) |
uint256 | minted ERC-20 balance — stale between interactions, do not use as voting power |
rawTotalSupply() |
uint256 | sum of minted balances — always >= totalSupply() |
Locks¶
| Function | Returns | Notes |
|---|---|---|
getLocks(user) |
LockedBalance[] |
active positions only (non-zero amount, from the withdrawal cursor forward) |
lockedBalanceOf(user) |
uint256 | total RAAC principal, including expired-but-unwithdrawn |
getMaxLockEnd(user) |
uint40 | latest unlock time, or 0 if none |
getMinLockEnd(user) |
uint40 | earliest unlock time, or type(uint40).max if none — the only value ragequitLock accepts |
getGlobalLockInfo() |
(uint256, uint40, uint40) | totalLocked, maxLockEpochs, epochDuration |
getUserTotalLockedAtEpochEnd(user, epochEnd) |
uint256 | principal maturing at that week; returns 0 if the user has an open ragequit request |
getUserPointHistory(user) |
Point[] |
raw bias checkpoints |
getCheckpoint(user) |
Checkpoint[] |
block-indexed power checkpoints |
getMinLockAmount() |
uint256 | current minLockAmount |
Rewards¶
| Function | Returns | Notes |
|---|---|---|
getRewardTokens() |
address[] | includes tokens with a pending removal |
claimed(user, token) |
uint256 | cumulative claimed |
claimable(user, token, maxDistributions) |
uint256 | simulated claim over the next maxDistributions entries |
earned(user, token, maxDistributions) |
uint256 | claimed + claimable |
getMaxUnclaimedDistributions¶
getMaxUnclaimedDistributions(address _user, address _token) → uint256
Summary
Returns how many distributions a caller should pass as maxDistributions to reach the furthest distribution they can still claim from. Distributions where the user was the excludedAddress, had zero voting power, or has already claimed every epoch are skipped and do not extend the returned bound.
Reverts — TokenNotFound() if _token is not registered. Returns 0 for the zero address.
Ragequit¶
| Function | Returns | Notes |
|---|---|---|
ragequitRequests(user) |
RagequitRequest |
amount (net), grossAmount, fixedPenalityAmount, variablePenalityAmount, readyAt; all-zero when none is open |
Errors¶
| Error | Meaning |
|---|---|
TransferNotAllowed() |
attempted veRAAC transfer — the token is non-transferable |
AmountBelowMinimum() |
below minLockAmount / minIncreaseAmount |
InvalidLockDuration() |
epochs outside [1, maxLockEpochs] |
InvalidEpoch() |
epoch end in the past, or not the caller's minimum lock end |
MissingEpoch() |
no active position at the given epoch end |
LockCannotBeExtended() |
extension resolves to the same or an already-past week |
LockNotExpired() |
nothing expired to withdraw |
NoTokensLocked() |
caller has no principal |
PendingRagequitRequest() |
an exit is already in progress |
NotFound() |
no ragequit request to finalize |
CooldownNotElapsed() |
ragequit or reward-removal cooldown still running |
RemovalNotInitiated() |
finalizeRemoveRewardToken without an initiated removal |
TokenNotFound() / TokenAlreadyAdded() / TokenPreviouslyRemoved() |
reward token registry |
InvalidAmount() / InvalidParams() / InvalidParameterValue() / InvalidAddress() |
argument validation |
InvalidTimestamp() / InvalidBlockNumber() |
historical query in the future |
ValueTooLargeToCast() |
bias exceeds int128 |
PowerTooHigh() |
checkpoint value exceeds int128 |