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:
- Deposit rToken into StabilityPool → receive deToken (1:1)
- Stake deToken in Curve liquidity pool → receive Curve LP tokens
- 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¶
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.
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
SafeERC20for secure transfers. - Liquidations executed via delegatecall to the liquidation module → upgradeable strategies without fund migration.
- Manager addresses stored on-chain for easy enumeration.
- Configurable
LiquidationVaultTokenSplitprovides 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,withdrawanddeposit - LiquidationStrategyProxy: delegate-called liquidation logic, executing in this pool's storage
- LiquidationSwap: sells the seized index token, slippage-bounded by
PmUSDToUSDOracleagainst the AMM pool - RWA Vault (
IBaseHybridVault): stores seized collateral (NFTs/tokens) and mints the index token - Asset adapters:
IAssetAdapteron the borrower side; on the vault side the adapter is alwaysRAACNFTVaultAdapterV2(anERC721VaultAdapter) — 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
notBlacklistedmodifier
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:
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 whichrToken.balanceOf(pool)exceedsdeToken.totalSupply(). - While a deficit is open that delta is zero, so no
rTokencan 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:
- Lending yield — the position still tracks the liquidity index, so it keeps earning LendingPool interest throughout
- Curve trading fees — from the
rToken/deTokenpool - CRV emissions — boosted up to 2.5×, obtained through Convex without locking CRV
- 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¶
- Upgradeable & Pausable – owner can pause all state-changing functions.
- Manager Role – multiple addresses can act on behalf of the pool without ownership transfer.
- 1 : 1 Accounting – deToken ↔ rToken peg is fixed.
- Flexible Liquidation Flow – new strategies can be deployed with no downtime.
- Withdrawal Timelock – prevents panic withdrawals during liquidations.
- Dust Collection – owner can collect excess tokens (only delta between deToken and rToken for rToken).
- Same-Block Protection – prevents deposit and withdrawal in the same block.
- 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)