Skip to content

RAAC Peg Stability Module (PSM) Vault

Overview

The PSMVault is RAAC's peg-defense and reserve vault for pmUSD. It pairs a yield-bearing savings token (the sUSDS Sky vault in v1) with a one-way swap mechanism that lets arbitrageurs convert discounted pmUSD back into a yield-bearing stablecoin at a favorable internal $1 valuation. Depositors supply sUSDS and receive a vault share token (rpsm-sUSDS) that keeps earning the native sUSDS savings rate and layers PSM revenue on top — swap fees, retained withdrawal fees, and drip-streamed protocol donations all accrue directly into the share price.

The vault is an ERC-4626-inspired share token with a custom deposit path (sUSDS in, not pmUSD) and a proportional, multi-token withdrawal path. It is upgradeable behind a TransparentUpgradeableProxy.

How the vault works in one minute

  • Deposit sUSDS, receive rpsm-sUSDS shares.
  • Share price accrues from swap fees, retained withdrawal fees, sUSDS yield, and drip-streamed protocol donations.
  • Exits use a short cooldown and return a proportional basket of the vault's current reserves (sUSDS plus any pmUSD the vault is holding) — so every shareholder always owns the same asset mix.
  • The dynamic withdrawal fee scales with composition: when sUSDS reserves are ample, exits are cheap; when pmUSD is accumulating, the fee rises and stays in the vault, compounding for remaining shareholders.

Current fee rates, drip duration, and cooldown windows are listed on the PSM Parameters page.

No direct pmUSD deposits

deposit() accepts sUSDS only. pmUSD enters the vault exclusively through swaps and authorized donate() calls. There is no mint() path and no ERC-4626 deposit(assets, receiver) that accepts pmUSD.

Parameters are governance-tunable — always check the live values

Every numeric value on this page (swap fee, withdraw fee bounds, drip duration, cooldown, reserve ratio floor, NAV floor, etc.) is a governance-controlled parameter, not a hard-coded constant. The values quoted here reflect the mainnet v1 launch config and may be updated at any time by GOVERNANCE_ROLE. For the authoritative, up-to-date values, always refer to the PSM Parameters page.


Purpose

  • Restore the pmUSD peg by creating synthetic redemption: arbitrageurs buy pmUSD below $1 on Curve, swap it at the PSM for sUSDS at an internal $1 valuation, and push the market price toward par.
  • Stack yield on top of yield — depositors earn the native sUSDS savings rate plus swap fees, retained withdrawal fees, and drip-streamed protocol donations. Capital keeps earning its baseline yield while doing the work of defending the peg.
  • Distribute protocol revenue via drip-streamed donate() calls, preventing MEV extraction of donated value.
  • Stay composablerpsm-sUSDS is a standard ERC-20 and can be used in any gauge, lending market, or yield-tokenization protocol that accepts ERC-20s.

Why the PSM exists

The PSM gives pmUSD a par redemption venue against a yield-bearing savings token. Any holder can swap pmUSD into the vault and receive a dollar's worth of sUSDS for every pmUSD they send in (minus a 25 bps swap fee). This par guarantee is the arbitrage floor that pulls the pmUSD market price back toward $1 whenever it trades at a discount.

The PSM only becomes the marginal redemption point once the Curve pool price drifts past the swap fee. While pmUSD trades within 1 − swapFee of par (i.e. above $0.9975 with the current 25 bps fee), arbitrageurs get a better execution on Curve than through the PSM, and the vault sees no flow — the pool's own two-sided liquidity does the work. Once the Curve price drops below that threshold, the PSM strictly dominates: buying pmUSD on Curve and redeeming at par through the vault yields risk-free profit, and the vault absorbs discounted pmUSD until the arbitrage closes and the market price returns above the fee threshold. The swap fee is therefore both the PSM's revenue knob and the activation threshold — raising it widens the band where Curve handles price discovery alone; lowering it narrows that band and makes the PSM the active redemption venue sooner. The current value is live on the Parameters page.

Three actors interact with the vault:

  • Arbitrageurs buy pmUSD at a discount on Curve and call swap(pmUSD → sUSDS) at the PSM. Every pmUSD sent in is valued at $1 internally; the outgoing sUSDS amount is priced through the RateProvider. They keep the difference between the market discount and the par valuation. Swaps are one-way — sUSDS cannot be exchanged back into pmUSD, because sUSDS reserves are precisely what funds the redemption floor.
  • Depositors supply sUSDS via deposit() and receive rpsm-sUSDS shares. Their sUSDS is the reserve that makes the par guarantee enforceable. In exchange, they earn swap fees, retained withdrawal fees, the native sUSDS yield, and drip-streamed protocol donations — all of which compound into the share price. Exits use a 1-day cooldown and a 1-day claim window, and return a proportional slice of the live reserve mix.
  • The protocol holds the DONOR_ROLE and routes revenue into the vault via donate(), which streams pmUSD into totalAssets() linearly over dripDuration. This is how protocol earnings accrue to depositors without being front-runnable.

Pricing sUSDS in pmUSD terms is handled by the RateProvider, which composes sUSDS's on-chain convertToAssets(1e18) with a Chainlink USDS/USD feed (staleness threshold on the Parameters page). Because pmUSD is asserted to equal $1 internally, no pmUSD/USD oracle is needed — one full leg of oracle risk is eliminated by design.

Two optional contracts are staged but not wired at launch: a baseAssetModule (an external ERC-4626 venue that would yield on idle pmUSD) and a rebalancer (an automated pmUSD → USDS → sUSDS recycler that refills the reserve after large swap inflows). Both addresses are address(0) in the v1 deployment — the redemption mechanism is live; the yield-routing surface is staged for later governance action.

Yield on top of yield

A key consequence of denominating the reserve in sUSDS — a savings token that already earns Sky's native yield — is that depositors in the PSM earn on top of the yield they would have earned holding sUSDS directly. The vault passes sUSDS's underlying appreciation through into totalAssets() (via convertToAssets), and then layers PSM-specific revenue on top: swap fees retained from every peg-restoration swap, withdrawal fees retained on every exit, and drip-streamed protocol donations. None of these are taken by an external fee collector on the exit path — they all compound directly into the rpsm-sUSDS share price.

The drip-streamed donations are not a fixed subsidy — they are sourced from RAAC's broader revenue base across multiple venues. Over time this includes yield generated by the protocol's Curve pool positions (trading fees and gauge emissions on pmUSD-paired pools), cashflows from the real-estate collateral backing pmUSD (interest and principal repayments on tokenized property loans), lending-pool protocol fees, and any other venue the treasury chooses to route through the Fee Collector and into donate(). As RAAC's revenue mix grows, the PSM becomes a single, composable conduit through which that diversified revenue reaches rpsm-sUSDS holders — in smooth, MEV-resistant drips rather than lump payouts.

Depositors therefore don't trade yield for peg security; they get the savings-rate yield plus a share of RAAC's protocol-wide revenue stream, while their capital is doing the work of defending the peg.


Core Mechanics

Depositor (supply sUSDS, earn)

  1. Approve sUSDS to the vault.
  2. Call deposit(savingsAmount, minSharesOut, receiver) — receive rpsm-sUSDS shares.
  3. Wait. Share price grows from swap fees, retained withdrawal fees, and drip-streamed donations.
  4. Call requestExit(shares) → wait withdrawTime → call redeem(...) or withdraw(...) inside the withdrawTimeLimit window.

Arbitrageur (peg restoration)

  1. Buy pmUSD on Curve at a discount (e.g. $0.95).
  2. Approve pmUSD to the vault.
  3. Call swap(pmUSD, sUSDS, amountIn, minAmountOut, receiver). The vault values the incoming pmUSD at $1, deducts the current swapFee (see Parameters), and sends sUSDS priced via the RateProvider.
  4. Pocket the difference between market price and internal valuation.

One-way by design

Swaps are pmUSD → sUSDS only. The PSM is a peg-defense venue, not a two-way AMM — reserve sUSDS is deployed to restore the peg, not to be recycled back into pmUSD on demand.

Donor (protocol revenue)

The DONOR_ROLE address calls donate(amount) with pmUSD. The donation is drip-streamed into totalAssets() linearly over dripDuration (28 days). This prevents deposit-donate-withdraw sandwich attacks.


Withdrawal Mechanics

Proportional, multi-token exit

When you redeem, the vault returns assets in the same ratio it currently holds them, in pmUSD-equivalent terms. If the vault is 70% sUSDS and 30% pmUSD at the time you claim, a \(1,000 exit returns ~\)700 of sUSDS and ~$300 of pmUSD (minus fee). You may therefore receive up to two distinct tokens in a single withdrawal under the v1 launch config (module shares become a possible third token once baseAssetModule is set).

Cooldown window (Resupply-style):

  1. requestExit(shares) — shares are transferred from you into the vault and a RedeemRequest is stored with exitTime = block.timestamp + withdrawTime.
  2. Wait until block.timestamp >= exitTime.
  3. Claim via redeem(shares, receiver, minAssetsOut) or withdraw(assets, receiver, maxSharesBurned) before exitTime + withdrawTimeLimit.
  4. If you miss the window, call cancelExit() to recover your shares and submit a new request.

Dynamic withdrawal fee:

feeBps = minWithdrawFee + (pmUSDValueInPool / totalAssets) * (maxWithdrawFee - minWithdrawFee)
  • Healthy vault (mostly sUSDS) → fee approaches minWithdrawFee (0.5% at launch).
  • Stressed vault (mostly pmUSD) → fee approaches maxWithdrawFee (3% at launch).
  • The fee stays inside the vault and accrues to remaining shareholders.

minWithdrawFee and maxWithdrawFee are both governance-tunable — check the Parameters page for the live values.


Key Functions

Function Access Summary
deposit(savingsAmount, minSharesOut, receiver) public Deposit sUSDS, mint rpsm-sUSDS. Reverts if sharesOut < minSharesOut.
swap(tokenIn, tokenOut, amountIn, minAmountOut, receiver) public pmUSD → sUSDS only. Deducts swapFee. Honors reserveRatioFloor and (if enabled) navFloor.
requestExit(shares) public Lock shares in the vault and start the cooldown. Reverts if a request is already pending — call cancelExit() first.
redeem(shares, receiver, minAssetsOut) public Burn locked shares for a proportional basket. Slippage-protected.
withdraw(assets, receiver, maxSharesBurned) public Burn the minimum shares needed to deliver assets pmUSD of net value. Slippage-protected.
cancelExit() public Recover locked shares if the cooldown window expired or you want to abort.
donate(amount) DONOR_ROLE Stream pmUSD revenue into the share price over dripDuration. Min donation: 1 pmUSD.
accrue() public Settle drip state. Called automatically at the start of every state-changing function.
totalAssets() view Live — sUSDS (oracle-valued) + pmUSD held − unvested drip + module position.
setSwapFee, setWithdrawFeeBounds, setWithdrawTime, setWithdrawTimeLimit, setReserveRatioFloor, setNavFloor, setCircuitBreakerEnabled, setRateProvider, setBaseAssetModule, setRebalancer GOVERNANCE_ROLE Parameter updates.
pause / unpause GUARDIAN_ROLE Emergency circuit-breaker.

Design Notes

Internal $1 valuation for pmUSD

The vault prices pmUSD at $1 in its own accounting. This is the mechanism that makes peg restoration work: arbitrageurs have a reliable, predictable venue to redeem pmUSD at par, which pulls the market price toward $1. The navFloor circuit breaker is available to governance to halt inbound swaps if pmUSD's NAV were ever to drop below a configurable threshold.

Proportional exits keep the reserve fair

Every shareholder's claim is always a proportional slice of the live reserve mix, so early exiters cannot cherry-pick the most liquid asset and leave remaining holders with the rest. This is standard PSM design (Spark PSM, MakerDAO LitePSM) and is what allows the vault to keep defending the peg without fragmenting its reserves.

Dynamic fee compounds into share price

Withdrawal fees (scaling with composition between minWithdrawFee and maxWithdrawFee) and the swap fee stay inside the vault. They are not taken by a fee collector on the exit path — they directly increase totalAssets() per share for remaining holders. Current rates: Parameters page.

Donations vest over the drip window

Protocol donations via donate() stream linearly over dripDuration (governance-configurable; see Parameters). This prevents MEV bots from sandwiching large donations, and aligns donated revenue with long-term depositors rather than flash participants.

Claim window

After the withdrawTime cooldown, there is a withdrawTimeLimit window to execute the claim. If it's missed, cancelExit() returns the locked shares and a fresh request can be submitted. Governance retains withdrawTimeLimit = 0 as an emergency lever to expire all pending claims. Current values: Parameters.

Inflation-attack protection

The vault uses OpenZeppelin's ERC-4626 decimal-offset pattern (virtual shares & assets) and is seeded on deployment, neutralizing the classic first-depositor inflation attack.


Implementation Details

  • Built with OpenZeppelin upgradeable modules: Initializable, ERC4626Upgradeable (with decimal offset for inflation protection), AccessControlUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable.
  • Deployed behind a TransparentUpgradeableProxy; implementation is upgradeable by the ProxyAdmin owner.
  • totalAssets() is computed live on every call — no stored accumulator. sUSDS is valued via the RateProvider, pmUSD is valued at $1, unvested drip is subtracted from baseAsset.balanceOf(this) so donations reveal over time without a separate reserve variable.
  • Every state-changing function begins with accrue() to settle the drip before modifying balances.
  • Share accounting uses OpenZeppelin's decimal-offset pattern (virtual shares + virtual assets) to neutralize first-depositor inflation attacks.
  • Rounding is always against the caller and in favor of the vault (two helpers: _convertToSharesRoundUp, _convertToAssetsRoundUp). The RateProvider exposes getRate() (Floor) for valuation and getRateRoundUp() (Ceil) for conversions where the rate is a denominator.
  • Cooldown uses the Resupply InsurancePool pattern: one pending request per user, exitTime snapshot at request time, bounded claim window.
  • Two independent circuit breakers: reserveRatioFloor (reserve-based) and navFloor (NAV-based, toggleable). Either one pauses pmUSD → sUSDS swaps.

Interactions

  • pmUSD (FractionalToken) — base asset, valued at $1 internally, entering only via swap() and donate().
  • sUSDS — sole deposit asset; ERC-4626 savings token whose convertToAssets(1e18) feeds the RateProvider.
  • RateProvider — composes sUSDS.convertToAssets() with the USDS/USD Chainlink feed. See PSM Parameters.
  • Chainlink USDS/USD feed — staleness-checked via stalenessThreshold.
  • Base-asset module (optional, ERC-4626 pmUSD yield vault) — currently address(0).
  • Rebalancer (optional, pmUSD → sUSDS recycler) — currently address(0).
  • Fee Collector — protocol fees accumulate here and are routed back into the vault via donate() under DONOR_ROLE (drip-streamed over 28 days).
  • PSM Parameters — all deployed addresses and current parameter values.
  • RWf(x) — the pmUSD issuance system whose peg this vault defends.
  • Fee Collector — upstream fee sink for the donate-back loop.