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¶
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:
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:
- Automatically, from harvested rewards, on every state-changing interaction.
- By donation —
donatepushes RAAC straight into the reward stream. - 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¶
- Upgradeable —
TransparentUpgradeableProxy,__gap[47], initializers disabled in the constructor. - Lock-parameter binding —
initializereadsgetGlobalLockInfo()from veRAAC and reverts withLockParamsMismatchunless the epoch duration matches andmaxLockEpochsis non-zero. - Non-blocking expired-lock processing —
processVaultExpiredLockswrapsveRAAC.withdraw()in atry, so a keeper is never blocked when nothing has matured. - Conservative relock buffer — only RAAC above
totalUnlockedGlobal + pendingCumulatedAmountis relocked, and only if it clears veRAAC's minimum lock amount. - Reward-token guard on gauge deposits —
depositToGaugerejects any token that veRAAC lists as a reward token, so gauge LP accounting cannot collide with the reward stream. - Fee-share invariant —
updateFeeSharesreverts withFeeSharesNotMatchingTreasuryFeeunless the three legs sum exactly to the treasury fee.