Skip to content

StabilityPool

Overview

StabilityPool is a core contract of the RAAC protocol that acts as the first line of defence for the LendingPool during liquidations. Users can deposit rToken (currently rpmUSD) into the pool and receive a 1 : 1 representation called deToken. These deposits are used to repay the debt of unhealthy borrowers that are liquidated through a dedicated strategy contract.

In return, depositors maintain exposure to the underlying rToken and can withdraw it at any time while the contract is not paused.

Token Mechanics & Yield Distribution

The StabilityPool maintains a 1:1 relationship between rToken and deToken:

  • rToken is the yield-bearing token deposited by users
  • deToken is the representation token minted 1:1 for deposits
  • deToken increases in value as the underlying rToken accrues yield from the lending pool
  • This ensures depositors earn the normal yield from the lending pool while maintaining their position in the StabilityPool

Staking deToken for Additional Yield

After receiving deToken from the StabilityPool, users can further stake their deToken in the Curve liquidity pool (DEToken/RToken).
This provides exposure to Curve LP tokens, which can then be staked in the RAAC Gauge to earn protocol emissions and rental yield.
This layered staking approach allows depositors to maximize their yield by participating in both Curve and RAAC incentive programs.

Flow:

  1. Deposit rToken into StabilityPool → receive deToken (1:1)
  2. Stake deToken in Curve liquidity pool → receive Curve LP tokens
  3. Stake Curve LP tokens in RAAC Gauge → earn emissions & rental yield

This mechanism is optional and designed for users seeking additional yield opportunities beyond the base LendingPool rewards.

Architecture

INHERITED — STORAGE & MIXINS MATH POOL TOKENS EXECUTES IN StabilityPool STORAGE EXTERNAL BOUNDARY StabilityPoolStorage rToken · deToken · rwaVault · treasury withdrawTimelock · __gap[50] WithCompliance notBlacklisted() OpenZeppelin (upgradeable) Initializable · Ownable · Pausable ReentrancyGuard · ERC721Holder StabilityPool rToken ⇄ deToken share accounting withdraw timelock · liquidation entry WadRayMath rayMul · rayMulDown · rayDiv The pool holds no interest math of its own — every index it reads comes from RToken / DEToken. RToken the pool’s underlying deposit DEToken share of the pool, index-scaled LiquidationStrategyProxy seize · sell · repay the reserve LendingPool finalize · deposit · withdraw IAssetAdapter borrower-side collateral IBaseHybridVault ERC-20 + NFT shares ILiquidationSwap swap(amount) IFeeCollector collectFee(...) ERC20 / ERC721 AssetAdapter RWAVault mints index token vs. collateral LiquidationSwap slippage-guarded exchange FeeCollector · Treasury liquidity rewards split RAACNFTVaultAdapterV2 vault-side custody (ERC-721) ILiquidityPool AMM pool · exchange() PmUSDToUSDOracle pmUSD → USD price using for deposit / withdraw rToken balanceOf() mint · burn shares getUserIndex() delegatecall finalizeLiquidation · withdraw · deposit validate position deposit collateral swap → pmUSD collectFee()

Purpose

  • Accept user deposits of rToken and issue deToken (1 : 1 peg)
  • Provide liquidity that covers borrower debt during liquidations
  • Delegate the actual liquidation logic to the liquidation module
  • Maintain a list of managers allowed to trigger liquidations
  • Route seized collateral to the RWA Vault and handle accounting via the Treasury contract
  • Support flexible configuration of how collateral proceeds are split (mint, burn, swap, liquidity)

Manager-Only Liquidations & No Incentives

Only delegated managers (not the general public) are permitted to trigger liquidations in the StabilityPool.
There is no direct incentivization for performing liquidations—this is a deliberate design choice due to the large size and complexity of the underlying assets.

Liquidations are expected to be carried out timely by managers to ensure sufficient liquidity is available in the Curve LP to process liquidations efficiently.
Managers are responsible for monitoring the system and considering factors such as gas fees and current slippage before executing a liquidation.
This approach helps maintain protocol stability and avoids unnecessary or poorly-timed liquidations that could negatively impact the system or its users.

Withdrawal Timelock Mechanism

The StabilityPool implements a withdrawal timelock to prevent panic withdrawals during liquidations:

  • Users must request withdrawal via requestWithdraw() before they can withdraw
  • A timelock duration must pass before the withdrawal becomes available
  • Users have a limited time window to execute their withdrawal after the timelock expires
  • Depositing cancels any pending withdrawal request
  • This mechanism ensures liquidity stability during critical liquidation periods

Timelock Configuration: - withdrawTimelockDuration: Time before withdrawal becomes available (initialize(): 30 minutes — live: 14 days) - withdrawTimelockDelay: Time window to execute withdrawal after the cooldown (initialize(): 2 days — live: 3 days) - Both parameters are configurable by the owner; setting withdrawTimelockDuration to 0 disables the cooldown entirely


Functions

Every external function — deposits and withdrawals, liquidation, administration, configuration and views — is documented with signatures, parameters, errors and source on the dedicated page.

→ StabilityPool — Functions

Accounting Simplicity

deToken is always minted/burned 1 : 1 with rToken, ensuring no internal exchange rate math or interest accrual. The peg is an invariant of the token, not a guarantee that the pool is fully backed — see Backing, Bad Debt and the Treasury Backstop.

Same-block transfers are blocked

After depositing the main asset and receiving newly minted DEToken, you cannot transfer those tokens to another address within the same transaction/block. Avoid attempting an DEToken transfer in the same transaction as your deposit.

The pool is Ownable

Administration is owner-gated, with managers as a narrower set holding liquidateBorrower and setLiquidationVaultTokenSplit. There are no governance parameters on this contract: everything configurable is a risk parameter the protocol controls for liquidations, so none of it sits behind a vote or a timelock.


Expand to View Implementation Details
  • Built with OpenZeppelin’s upgradeable modules: Initializable, OwnableUpgradeable, PausableUpgradeable.
  • Protected with ReentrancyGuard.
  • Uses SafeERC20 for secure transfers.
  • Liquidations executed via delegatecall to the liquidation module → upgradeable strategies without fund migration.
  • Manager addresses stored on-chain for easy enumeration.
  • Configurable LiquidationVaultTokenSplit provides flexible handling of collateral proceeds.
Expand to View Interactions
  • RToken: ERC20 deposited by users (e.g. rpmUSD); the pool's backing asset
  • DEToken: ERC20 representing depositor shares, minted 1 : 1
  • LendingPool: source of the liquidity index, target of finalizeLiquidation, withdraw and deposit
  • LiquidationStrategyProxy: delegate-called liquidation logic, executing in this pool's storage
  • LiquidationSwap: sells the seized index token, slippage-bounded by PmUSDToUSDOracle against the AMM pool
  • RWA Vault (IBaseHybridVault): stores seized collateral (NFTs/tokens) and mints the index token
  • Asset adapters: IAssetAdapter on the borrower side; on the vault side the adapter is always RAACNFTVaultAdapterV2 (an ERC721VaultAdapter) — no ERC-20 vault adapter is deployed
  • FeeCollector / Treasury: receive the liquidity-reward and minting-fee legs, and back the pool operationally
  • Compliance Registry: blocks blacklisted addresses via the inherited notBlacklisted modifier

Backing, Bad Debt and the Treasury Backstop

deToken is minted and burned 1 : 1 against rToken, and both track the same liquidity index. In normal operation the pool holds exactly one rToken for every deToken in circulation.

A liquidation can break that equality. If the pmUSD raised from the seized collateral does not cover the borrower's debt, LiquidationStrategyProxy closes the gap by calling lendingPool.withdraw(), which burns rToken held by the StabilityPool. No deToken is burned on that path, so afterwards:

rToken.balanceOf(StabilityPool)   <   deToken.totalSupply()

The pool is under-backed by exactly the shortfall. This is bad debt, and absorbing it is precisely what the StabilityPool exists to do — it is the protocol's first-loss layer, ahead of depositors in the LendingPool.

How a deficit behaves

Withdrawals stay 1 : 1. The contract does not re-price deToken against the remaining rToken. Depositors who withdraw while a deficit is open are paid in full; if the pool is drained the rToken.safeTransfer in withdraw() simply reverts. The loss is therefore not socialised pro-rata — see Design Decisions for why.

A deficit compounds. rToken and deToken grow at the same liquidity index. Because the pool holds fewer raw rToken than the raw deToken supply, interest accrues on unbacked deToken with no matching rToken growth, so the gap widens on every index update. A 2,000 deficit at index 1.00 is a 2,200 deficit by the time the index reaches 1.10.

A profitable liquidation repairs it first. Surplus pmUSD from a profitable liquidation is re-deposited into the LendingPool, minting fresh rToken to the StabilityPool without minting any deToken. That surplus therefore closes the deficit before it becomes withdrawable value for anybody.

Restoring backing from the Treasury

Backing is restored by raising rToken.balanceOf(StabilityPool) without minting deToken — that is, by transferring rToken directly to the pool contract rather than calling deposit(). Calling deposit() would mint deToken 1 : 1 to the depositor and leave the ratio exactly where it was.

The Treasury can therefore repair a deficit at any time out of treasury holdings and accumulated protocol revenue, by sending rToken — or pmUSD deposited into the LendingPool for rToken — straight to the StabilityPool address.

The Treasury backstop is an operational action, not an automatic code path

Nothing inside liquidateBorrower pulls funds from the Treasury. The Treasury can act at two points: before a liquidation, by sending pmUSD to the Stability Pool so the debt is covered without touching the pool's rToken (or by injecting pmUSD into the LendingPool so the pool's rToken withdrawal can clear under high utilisation), and after it, by restoring the backing as described above. See Covering a shortfall.

collectDust() is the guard on the other side of that operation:

  • For rToken, the owner can only take the delta by which rToken.balanceOf(pool) exceeds deToken.totalSupply().
  • While a deficit is open that delta is zero, so no rToken can leave the pool.
  • Treasury funds sent in to repair backing therefore cannot be swept back out until backing is whole again.

Cooldown: why a deficit does not become a bank run

Because withdrawals are 1 : 1 and a deficit is never re-priced, a depositor who learns of bad debt first would otherwise be strictly better off exiting immediately, and the pool would empty from the front. The withdrawal cooldown is the mechanism that removes that race.

Step Behaviour
requestWithdraw(amount) Queues an exit and pins balanceAtRequest and indexAtRequest. A request for 0 cancels.
Cooldown — withdrawTimelockDuration The request is not executable until readyAt = now + duration. initialize() sets 30 minutes; the live value is 14 days — see Parameters.
Execution window — withdrawTimelockDelay withdraw() is only valid in [readyAt, readyAt + delay]. initialize() sets 2 days; the live value is 3 days. After the window the request expires and must be re-queued.
Amount is pinned withdraw(amount) must match the queued amount exactly, and is capped at the requested balance plus the interest accrued on it since the request.
Depositing cancels Any deposit() deletes a pending request, so a request cannot be front-loaded before holding a balance.
Same-block guard Deposit and withdraw cannot happen in the same block.
Pause deposit, requestWithdraw and withdraw are all whenNotPaused, so the owner can halt exits outright; liquidateBorrower deliberately stays available while paused.

The cooldown means no depositor can exit inside the window in which a bad liquidation is executed and settled. That buys the protocol the time it needs to repair backing from the Treasury or from the next profitable liquidation, so that the queued withdrawals which land after the cooldown are fully backed again.


The deToken Yield Stack

Planned — not yet deployed

The Curve pool and the two gauges below are upcoming. None of the addresses are live.

rToken and deToken both rebase — balanceOf grows with the LendingPool liquidity index without a transfer. The pool is therefore deployed as Curve Stableswap-NG with asset_type = 2 (rebasing) for both coins, so the accrual reads through balanceOf and accrues to LPs.

Layer 1 — the Curve stableswap pool

Property Value Why
Pool type Stableswap-NG Both legs are claims on the same reserve asset at the same index
Asset type 2 — rebasing balanceOf grows on its own; NG reads it so the accrual reaches LPs
Weighting 50 / 50 The two legs are 1 : 1 by construction
Rate handling None required Both tokens scale by the same getNormalizedIncome(), so the legs move together

Layer 2 — the Curve gauge

RAAC will provide a Curve gauge for the pool, so LP tokens can be staked for CRV emissions on top of trading fees.

This layer carries Curve's own boost, which scales rewards up to 2.5× with the staker's veCRV. Routing through Convex delivers that boost without the LP locking CRV themselves.

Layer 3 — the RAAC gauge

On top of the Curve position, RAAC will run a RAAC Gauge distributing RAAC ecosystem emissions to the same LP position, with its own boost curve driven by veRAAC:

Constant Value Meaning
GaugeController.MIN_BOOST 10000 1.0× — no veRAAC
GaugeController.MAX_BOOST 25000 2.5× — maximum veRAAC weight

Emissions claimed from the RAAC Gauge are not paid in raw $RAAC: they are deposited into the leRAAC Liquid Locker and returned as $leRAAC minted at 50% LTV. See RAAC Token — Distribution Mechanism.

Why this is the highest-yield position in the system

A depositor who takes the full stack collects four streams at once:

  1. Lending yield — the position still tracks the liquidity index, so it keeps earning LendingPool interest throughout
  2. Curve trading fees — from the rToken / deToken pool
  3. CRV emissions — boosted up to 2.5×, obtained through Convex without locking CRV
  4. RAAC emissions — boosted up to 2.5× by veRAAC, paid as $leRAAC

The two 2.5× boosts do not multiply

They apply to different emission streams: the Curve-side boost scales CRV, the RAAC-side boost scales RAAC. A staker at maximum weight on both sides earns 2.5× on each stream separately — not 6.25× on a single one.

The stack is why withdrawal requests are unescrowed

requestWithdraw() does not lock the deToken being requested, precisely so a depositor can keep it working in the pool and the gauges throughout the cooldown. See Design Decisions.


Design Decisions & Accepted 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.

→ StabilityPool — Design Decisions & Accepted Trade-offs


Additional Features & Notes

  1. Upgradeable & Pausable – owner can pause all state-changing functions.
  2. Manager Role – multiple addresses can act on behalf of the pool without ownership transfer.
  3. 1 : 1 Accounting – deToken ↔ rToken peg is fixed.
  4. Flexible Liquidation Flow – new strategies can be deployed with no downtime.
  5. Withdrawal Timelock – prevents panic withdrawals during liquidations.
  6. Dust Collection – owner can collect excess tokens (only delta between deToken and rToken for rToken).
  7. Same-Block Protection – prevents deposit and withdrawal in the same block.
  8. Compliance Integration – blacklisted addresses cannot interact with the pool.

Liquidation Responsibility

Liquidation is only triggered through the liquidation module. Managers and owners must ensure correct configuration, as seized collateral flows directly to the RWA Vault.

Dust Collection Safety

The collectDust function has built-in safety for rToken: - Only the delta between deToken total supply and rToken balance can be collected - This prevents accidental removal of user funds - Other tokens can be collected in full (use with caution)