Skip to content

RAACLiquidLocker

Overview

RAACLiquidLocker is the contract behind $leRAAC. It accepts RAAC, max-locks it into veRAAC on a rolling 52-epoch schedule, and lets depositors borrow leRAAC against that locked collateral at a 50 % reserve rate. Rewards harvested from veRAAC are swapped back into RAAC and used to pay the borrower's debt down automatically — which is what makes leRAAC a self-repaying, non-liquidating loan.

The locker is the single veRAAC position that the protocol votes with: user deposits are pooled into one escrow account, and gauge and governance votes are cast from it by a delegate.

The position never liquidates

There is no liquidation path and no oracle. A position that falls below the reserve rate is not seized — instead the locker relocks whatever portion of the matured collateral is needed to keep the debt backed, and lets the reward stream repay it over time. The only enforcement point is that a user cannot withdraw while doing so would leave bad debt behind.

Deposits are locked for a full year, and they roll

A deposit is locked into veRAAC for lockDurationEpochs (52 epochs ≈ 364 days). An unreserved position auto-relocks at maturity for another full term. Getting RAAC back requires calling requestUnlock before maturity to reserve it, then withdrawUnlocked once it matures.


Architecture

INHERITED — UPGRADEABLE MIXINS USER ASSETS & DEBT ESCROW · SWAP · FEES VOTING & OPERATIONS OpenZeppelin (upgradeable) Ownable · AccessControl · Pausable ReentrancyGuard · __gap[47] RAACToken deposited · relocked · rewards LeRAAC debt token · mint on borrow RAACMaturityVault leRAAC maturity & repayment RAACLiquidLocker pooled veRAAC position epoch unlock buckets · 50 % reserve self-repaying debt veRAACToken lock 52 epochs · withdraw · claim IZap reward → intermediate → RAAC Treasury · Clever · BotLocker harvest fee split GaugeController voteGauge · removePower Governance voteGovernance BaseGauge stake · withdraw · claimFor Keepers harvest · processExpiredLocks deposit · payout mint / burnFrom distribute() lock · withdraw · claimReward zap(token, amount) treasury fee · bounty DELEGATE_ROLE BOOSTER_ROLE HARVEST_ROLE · keeper

Purpose

  • Pool user RAAC into a single max-locked veRAAC position and track each depositor's share
  • Mint leRAAC against that collateral at a 50 % reserve rate, optionally straight into the Maturity Vault
  • Harvest veRAAC rewards, swap them to RAAC through the Zap, and use them to repay depositor debt automatically
  • Schedule unlocks on epoch boundaries, auto-relocking anything the user has not reserved
  • Cast the pooled position's gauge and governance votes through a delegate
  • Split harvest fees between the treasury, the Clever fee recipient and the bot locker

Collateral & Debt Model

The reserve rate

reserveRate is fixed at 500_000_000 against a FEE_PRECISION of 1e9 — 50 %. The invariant enforced at every mutation point is:

\[(\text{totalLocked} - \text{reserved}) \times \text{reserveRate} \;\ge\; (\text{totalDebt} + \text{newBorrow}) \times \text{FEE\_PRECISION}\]

which reduces to unreserved locked ≥ 2 × debt. Collateral that a user has already reserved for unlock is scheduled to leave, so it cannot back new debt — both borrow and requestUnlock net it out through the same _checkAccountHealth.

Rewards are spent before collateral

borrow draws from the caller's accrued rewards first. Anything covered by rewards is not recorded as debt — only the shortfall increases totalDebt. This is the self-repaying leg: a depositor who never borrows more than their reward stream never carries debt at all.

Repayment

Debt is repaid three ways, in descending order of how much they matter:

  1. Automatically, from harvested rewards, on every state-changing interaction.
  2. By donation — donate pushes RAAC straight into the reward stream.
  3. Manually, via repay, burning the caller's leRAAC.

repayFeePercentage is fixed at zero

repay charges repayFeePercentage, sends the fee to the treasury as leRAAC, and burns only the remainder — so a non-zero fee makes it impossible to clear a debt through repay alone. The team has settled on keeping the parameter at 0 in production for exactly this reason. See Design Trade-offs.


Unlock Scheduling

Each deposit lands in an epoch bucket — an EpochUnlockInfo holding the RAAC maturing at one week boundary. Same-week deposits merge into one bucket, which bounds the number of live entries to roughly lockDurationEpochs.

A bucket is split into two portions:

Portion Field Behaviour at maturity
Open pendingUnlock - reservedAmount auto-relocked for another full term
Reserved reservedAmount credited to totalUnlocked, withdrawable

requestUnlock reserves soonest-maturing buckets first, partial-reserving the boundary bucket. If reserving only part of a bucket would leave an open remainder below veRAAC's minLockAmount, the whole bucket is taken instead — a remainder that small could not be relocked.

stateDiagram-v2
    [*] --> Open: deposit() → bucket at epoch E
    Open --> Open: matures at E, auto-relocked to E+52
    Open --> Reserved: requestUnlock() reserves FIFO
    Reserved --> Matured: epoch E reached, credited to totalUnlocked
    Matured --> [*]: withdrawUnlocked() → payout
    Matured --> Open: relocked portion needed to back debt

Withdrawal splits before it pays

withdrawUnlocked does not simply pay out the matured amount. It computes the collateral still required to back the outstanding debt and relocks the shortfall, paying out only the remainder:

requiredLocked = ceil(totalDebt × FEE_PRECISION / reserveRate)
relock         = min(max(requiredLocked - totalLocked, 0), amount)
payout         = amount - relock

previewWithdraw runs the identical split, so the projection always matches the outcome.


Harvest & Distribution

sequenceDiagram
    participant K as Keeper (HARVEST_ROLE)
    participant L as RAACLiquidLocker
    participant V as veRAACToken
    participant Z as Zap
    participant M as RAACMaturityVault
    K->>L: harvest(recipient, minOut)
    L->>V: claimReward() per reward token
    L->>Z: zap(token → intermediate) then (intermediate → RAAC)
    Note over L: RAAC-denominated rewards bypass the Zap
    L->>L: minOut check, then treasury fee + harvest bounty
    L->>L: accRewardPerShare += amount × 1e18 / totalLockedGlobal
    L->>M: distribute(locker, amount)

The treasury fee is split three ways by feeSharesBots, feeSharesClever and feeSharesTreasury, which must sum exactly to treasuryFeePercentage. The bot-locker leg is attempted inside a try, and a specific revert code falls back to sending that leg to the treasury rather than failing the whole harvest.

Sub-share rewards accumulate rather than round away

If a distribution is small enough that amount × 1e18 / totalLockedGlobal truncates to zero, the amount is held in pendingCumulatedAmount and folded into the next distribution instead of being lost.

minimunOut only guards the Zap leg

The _minimunOut check is applied to the swapped amount before RAAC-denominated rewards are added back. Rewards already denominated in RAAC bypass the Zap entirely and are not covered by the slippage bound.


Parameters

Parameter Value Setter Notes
reserveRate 500,000,000 (50 %) — set in initialize, no setter
lockDurationEpochs 52 — read from veRAAC at initialize; reverts on mismatch
EPOCH_DURATION 7 days — constant; must equal veRAAC's epoch duration
FEE_PRECISION 1e9 — constant
repayFeePercentage 0 updateRepayFeePercentage capped at MAX_REPAY_FEE = 1e8 (10 %)
treasuryFeePercentage 20 % (deploy) updateFeeShares capped at MAX_TREASURY_FEE = 5e8 (50 %)
harvestBountyPercentage 3 % (deploy) updateHarvestBountyPercentage capped at MAX_HARVEST_BOUNTY = 1e8 (10 %)
feeSharesBots 1 % (deploy) updateFeeShares the three shares must sum to treasuryFeePercentage
feeSharesClever 1.25 % (deploy) updateFeeShares
feeSharesTreasury 17.75 % (deploy) updateFeeShares
maxDistribution off by default setMaxHarvestDistribution / toggleMaxDistributions caps reward claims per harvest

Access Control

Role Powers
owner (Ownable) all configuration setters, keeper whitelist, unpause
DEFAULT_ADMIN_ROLE role administration, donate, max-distribution toggles
HARVEST_ROLE harvest
DELEGATE_ROLE voteGauge, voteGovernance, removePowerFromGauge
OPERATOR_ROLE addGauge — grants ON_BEHALF_OF_ROLE to a gauge
ON_BEHALF_OF_ROLE depositOnBehalfOf for another account
BOOSTER_ROLE depositToGauge, withdrawFromGauge, claimRewardFor
PAUSER_ROLE pause
keeper whitelist processVaultExpiredLocks

The pause is asymmetric

PAUSER_ROLE can pause, but only the owner can unpause. Every user entry point — including withdrawUnlocked and repay — is whenNotPaused, so a pause freezes exits as well as entries.


Functions

Every external function — deposits, borrowing, unlocks, harvesting, gauge operations, administration and views — is documented with signatures, parameters, errors and source on the dedicated page.

→ RAACLiquidLocker — Functions


Design Trade-offs

Behaviours that a security review raised and the protocol accepted as design — rather than fixed — are documented separately, so that reviewers can tell a deliberate trade-off from a defect.

→ RAACLiquidLocker — Design Trade-offs


Additional Features & Notes

  1. Upgradeable — TransparentUpgradeableProxy, __gap[47], initializers disabled in the constructor.
  2. Lock-parameter binding — initialize reads getGlobalLockInfo() from veRAAC and reverts with LockParamsMismatch unless the epoch duration matches and maxLockEpochs is non-zero.
  3. Non-blocking expired-lock processing — processVaultExpiredLocks wraps veRAAC.withdraw() in a try, so a keeper is never blocked when nothing has matured.
  4. Conservative relock buffer — only RAAC above totalUnlockedGlobal + pendingCumulatedAmount is relocked, and only if it clears veRAAC's minimum lock amount.
  5. Reward-token guard on gauge deposits — depositToGauge rejects any token that veRAAC lists as a reward token, so gauge LP accounting cannot collide with the reward stream.
  6. Fee-share invariant — updateFeeShares reverts with FeeSharesNotMatchingTreasuryFee unless the three legs sum exactly to the treasury fee.