The reserve
The reserve is the money that makes a member default survivable. It lives in one contract,
AssurancePool, priced by AssuranceOracle, with token policy held in TokenRegistry. Bond
proceeds are not here — they sit in BondVault (Bonds) — and merchant payables are
not here either — they sit in PayoutPool (Settlement).
On this page
Deployed addresses are in the root README.md (Deployed addresses).
AssurancePool.solAssuranceOracle.solTokenRegistry.solITargetRTDSourceIExposureSourceCollateralRegistry implements itThree tiers
Only the primary reserve counts toward the RTD. The buffer and the excess are real money in the same contract, but the ratio never sees them.
primaryReservereimburse only, once the buffer is emptybufferReservereimburse first; lent by lendToPayoutPool / lendToBondVault while hasValidRTD()excessReservewithdraw / withdrawToken by an approved instrument; lent first by both lend functionsEach tier is one number in reserve-token units, keyed by the current reserveToken
(primaryBalance(), bufferBalance(), excessBalance()). The tokens themselves are commingled:
any accepted token can fund any tier, and its oracle value is what gets booked.
Every token the pool has taken in is recorded by _registerHeldToken, capped at
MAX_HELD_TOKENS = 16 (AssurancePoolTooManyTokens). heldReserveValue() prices that whole set
and is a monitoring view, not a revert condition: comparing it with the three tiers is the solvency
check. A token the oracle cannot price is skipped and counted by unpricedTokenCount() /
unpricedTokens(), so accounting understates rather than reverts. The payout path does not tolerate
this — it uses the reverting conversion, so an unpriceable token is never paid out.
RTD
RTD is the primary reserve divided by what it stands behind. AssurancePool.RTD():
RTD = primaryBalance × 1e18 / ( convertStableCreditToReserveToken(poolExposure()) + bondExposure() )poolExposure()exposureSource.poolExposure(), clamped to stableCredit.totalSupply(). With no source set, total supply — every credit treated as unsecuredbondExposure()BondVault.uncoveredNearTerm(): face value due inside the vault's window that neither the vault nor the treasury's allowance can pay. Zero while bondVault is unsettargetRTD()assuranceOracle.targetRTD()deploy/20 points exposureSource at CollateralRegistry, which excludes savings-backed debt,
counts asset-backed debt at its shortfall after haircut, and counts unsecured debt in full (see
Credit).
RTD() returns 0 when the primary reserve is empty or there is no exposure. Read hasValidRTD()
instead: it is true when exposure is zero, at any balance. neededReserves() is
(target − RTD) × exposure / 1e18 while below target, and 0 otherwise.
Target RTD
AssuranceOracle.targetRTD() returns targetRTDSource.targetRTD() when a source is registered
(setTargetRTDSource), and staticTargetRTD otherwise. deploy/09 initialises the static target
at 0.8e18; scripts/set_target_rtd.ts changes it.
setTargetRTD (oracle owner) writes staticTargetRTD and, when the new value is above the current
effective target and the pool holds excess, calls AssurancePool.reallocateExcessBalance() — the
oracle passes the pool's onlyOperator check.
Money in
depositRepayment(amount)StableCredit._routeRepayment sends it what is left after the PayoutPool's needneededReserves(), the rest to the bufferdeposit(amount) / depositToken(token, amount)neededReserves(), the rest to excessdepositIntoPrimaryReserve / depositIntoBufferReserve / depositIntoExcessReservedepositTokenIntoPrimary / …IntoBuffer / …IntoExcessA token is accepted (_isTokenAccepted) if it is the reserve token, an oracle stablecoin, or
whitelisted by the oracle.
Repayments fill the primary reserve to target first. Only the part of a repayment above
neededReserves() reaches the buffer. A repayment therefore stops filling primary once the target is
met, and an empty book (neededReserves() == 0) sends all of it to the buffer.
Moving between tiers
moveBufferToPrimary(amount)bufferBalance() into primary. The only way to build primary cover while no exposure is outreallocateExcessBalance()neededReserves()rebalanceRTD()rebalanceRTDPublic()Repricing (_updateReserveValues) moves nothing between tiers. A gain in held value lands in excess.
A loss comes out of excess, then buffer, then primary. While any held token is unpriceable it writes
nothing. Moving surplus out of primary (_rebalanceFromPrimary) sends half to the buffer when the
buffer is empty and the rest to excess; it never takes primary below targetRTD × exposure.
Money out
Lost debt
Lost debt is the only thing primary pays for. StableCredit.writeOffCreditLine (a credit
issuer) is the only place lostDebtOutstanding grows. A credit holder, or an operator for them,
retires it with StableCredit.burnLostDebt(member, amount):
- 1.
amountmust not exceed the member's balance orlostDebt(). - 2.The member's credits move to the lost-debt account and
lostDebtOutstandingfalls byamount. - 3.
AssurancePool.reimburse(member, amount in reserve token)(onlyStableCredit) pays from the buffer first, then primary, and never from excess.
Redemption is capped twice: by lost debt outstanding, and by primaryBalance + bufferBalance. When
the two reserves cannot cover the request, reimburse empties both and pays what they held; step 2
has already moved the full amount.
Excess withdrawals
withdraw and withdrawToken take from excess only, and only for an address on the
withdrawalCallers allowlist (onlyWithdrawalCaller, managed by setWithdrawalCaller, admin).
Operator or admin access is not enough. The list is empty on a fresh deployment and no deploy script
adds anyone.
withdrawToken pays in the requested token when the pool has enough of it, and otherwise makes up
the value through _withdrawEquivalentValue: the requested token, then withdrawalPriority() —
reserve token, the configured USDC / USDT / DAI (setTokenAddresses), then every other held token.
Both functions end in _assertLossAbsorptionUntouched, which reverts if primary or buffer moved.
Lending as a last resort
The pool lends spare money to the PayoutPool and the BondVault, never primary cover. Both
lendToPayoutPool and lendToBondVault take from excess first, then from the buffer only while
hasValidRTD() holds, and never from primary. Each is callable only by the address set with
setPayoutPool / setBondVault (operator), and returns what it could lend, which may be zero.
flowchart LR
subgraph PP["PayoutPool.drawForDueClaim"]
Y["LendingPool<br/><i>availableCash</i>"] --> BV["BondVault<br/><i>deployableExcess</i>"] --> T1["Treasury<br/><i>allowance</i>"]
end
subgraph CV["BondVault._cover"]
H["Vault holdings"] --> T2["Treasury<br/><i>allowance</i>"]
end
T1 --> AP["AssurancePool"]
T2 --> AP
AP --> E["excess"] --> B["buffer<br/><i>only if hasValidRTD()</i>"]
AP -. never .-> P["primary"]The pool is last in both waterfalls. A payout claim reaches it only after the lending pool, the bond
vault's deployable excess and the treasury's allowance to the PayoutPool; a maturing bond only after
the vault's own holdings and the treasury's allowance to the vault. The treasury is the Safe:
each contract reads min(allowance(treasury → contract), balanceOf(treasury)), and the Safe's
owners set those allowances themselves.
What is out is tracked in lentToPayoutPool / lentToBondVault, and the part that came from the
buffer in bufferOwed / bondBufferOwed. repayFromPayoutPool and repayFromBondVault pull the
repayment from the caller and restore the buffer first, the rest to excess. Carry the PayoutPool
pays on a reserve loan arrives through repayFromPayoutPool, so it lands in excess once the buffer
is whole. BondVault repays from new proceeds before the Safe can withdraw (see
Bonds).
Lending the buffer does not change the RTD, because the buffer is not in it. The bond gap the pool
backs is in it, through bondExposure().
Pricing — AssuranceOracle
The oracle prices from a time-weighted average, never from spot. getTokenPriceInUSD(token),
18 decimals, in order:
- 1.A stablecoin — the configured USDC / USDT / DAI, or one the registry flags — is
1e18. - 2.If
forceRegistryFallback[token]is set and the registry has a price, that price. - 3.
getUniswapPrice: the token's USDC pool at the 0.3% fee tier, or else its WETH pool times WETH/USDC. - 4.The registry's fallback price.
- 5.Otherwise revert
"No price data".
Pool prices come from _twapSqrtPriceX96, which calls observe over twapPeriod (default 1,800
seconds, settable from MIN_TWAP_PERIOD = 300 to MAX_TWAP_PERIOD = 86400) and rounds the average
tick down. A pool that cannot serve the window returns 0 — there is no slot0 fallback — and pricing
moves on to the registry, then fails closed.
quote(depositToken, reserveToken, amount) multiplies by getPriceRatio and rescales between the
two tokens' decimals. isTokenWhitelisted is true for the configured stablecoins and for anything the
registry whitelists.
setTargetRTD, setTargetRTDSourcesetForceRegistryFallback(token, force)setPricingAddressessetTwapPeriodTwapPeriodOutOfBounds)TokenRegistry
TokenRegistry is Ownable. registerToken whitelists a token and sets its fallback price
(1e18 when given 0); setFallbackPrice also whitelists; setStablecoin sets the flag the oracle
treats as $1; removeToken clears all of it. The oracle reads getIsWhitelisted,
getIsStablecoin, getFallbackPrice and getWhitelistedTokens. The pool never reads the registry
directly. Detail is in token-registry.md.
Authority
onlyAdminAccessManager adminsetReserveToken, setWithdrawalCaller, setExposureSource, setAssuranceOracle, setTokenAddressesonlyOperatorAccessManager operator, or the oraclemoveBufferToPrimary, reallocateExcessBalance, rebalanceRTD, setPayoutPool, setBondVaultonlyStableCreditreimburseonlyWithdrawalCallerwithdraw, withdrawTokenpayoutPool / bondVaultlendToPayoutPool / lendToBondVaultrebalanceRTDPublic, repayFromPayoutPool, repayFromBondVaultA token stays in the held set once it has arrived, so setReserveToken does not hide the outgoing
reserve token's balance from accounting or from the payout path. Both contracts are UUPS proxies; the
pool's fields added after its first deployment sit below every earlier one. The upgrade scripts for
the reserve are scripts/upgrade_assurance_reserves.ts (the three tiers and repayment routing),
upgrade_reserve_draw.ts and upgrade_bond_backstops.ts (lending to the PayoutPool and BondVault).