Skip to content

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 _amount RAAC 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
await raac.approve(veRAAC.target, parseUnits("1000", 18));
// lock 1,000 RAAC for 52 epochs (~1 year) → ~1,000 veRAAC
const tx = await veRAAC.lock(parseUnits("1000", 18), 52);
await tx.wait();
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
const locks = await veRAAC.getLocks(user);
await raac.approve(veRAAC.target, parseUnits("500", 18));
await veRAAC.increase(parseUnits("500", 18), locks[0].unlockTime);
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 remains
  • RewardNotified(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
Typescript / ethers
const n = await veRAAC.getMaxUnclaimedDistributions(user, rewardToken);
if (n > 0n) await veRAAC.claimReward(rewardToken, n);

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 threshold
  • RewardRedirectedToTreasury(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 MethodonlyOwner.

EmitsRewardTokenAdded(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 MethodonlyOwner.

Emits

  • RewardTokenRemovalInitiated(address indexed token, uint256 removalAvailableAt)
  • RewardTokenRemovalCanceled(address indexed token)

RevertsTokenNotFound() 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 MethodonlyOwner.

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.

RevertsTokenNotFound() 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