Fixed Lending - trUSD (Rolling Bond)

trUSD is Reservoir Protocol's term-based yield-bearing token, implemented as the RollingBond smart contract. Where rUSD is the base stablecoin and srUSD is the liquid savings product, trUSD introduces

tl;dr: Deposit rUSD β†’ receive trUSD shares that accrue yield continuously β†’ choose to redeem after a lock-up period (full yield, no fee) or redeem instantly at any time (immediate, with a small fee).

Where trUSD Fits in the Reservoir Ecosystem

Reservoir's four core primitives each serve a distinct role:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Reservoir Protocol                          β”‚
β”‚                                                                   β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚   rUSD   β”‚   β”‚  srUSD   β”‚   β”‚  trUSD   β”‚   β”‚  Lending   β”‚  β”‚
β”‚   β”‚          β”‚   β”‚          β”‚   β”‚          β”‚   β”‚  Market    β”‚  β”‚
β”‚   β”‚ Base     β”‚   β”‚ Liquid   β”‚   β”‚ Term     β”‚   β”‚            β”‚  β”‚
β”‚   β”‚ Stable-  β”‚β†’  β”‚ Savings  β”‚   β”‚ Rolling  β”‚   β”‚ SteakrUSD  β”‚  β”‚
β”‚   β”‚ coin     β”‚   β”‚ Variable β”‚   β”‚ Bond     β”‚   β”‚ (Morpho)   β”‚  β”‚
β”‚   β”‚          β”‚   β”‚ Yield    β”‚   β”‚ Higher   β”‚   β”‚            β”‚  β”‚
β”‚   β”‚ 1:1 USDC β”‚   β”‚ No lock  β”‚   β”‚ Yield +  β”‚   β”‚ Borrow /   β”‚  β”‚
β”‚   β”‚          β”‚   β”‚          β”‚   β”‚ Lock-up  β”‚   β”‚ Loop       β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

trUSD targets users who want higher yield than the liquid srUSD rate and are willing to accept a time commitment. Multiple deployments exist with different lock-up durations, giving users a spectrum of term/yield trade-offs.


The Rolling Bond Concept

Traditional term bonds have a fixed maturity date. When the bond matures, users must redeem and re-issue β€” creating friction and "cliff" events that disrupt yield continuity.

RollingBond eliminates maturity dates entirely:

Traditional Term Bond
RollingBond (trUSD)

Fixed maturity date

No maturity β€” yield accrues perpetually

Must re-issue periodically

Deposit once, earn indefinitely

Yield paid at maturity

Yield accrues continuously, second by second

Instant redemption at maturity

Redemption requires lock-up (or fee for instant)

Binary: locked OR redeemed

Dual path: standard (lock-up) OR early (fee)


Product Variants

A separate RollingBond contract is deployed for each term duration. Each instance is independently configured with its own lock-up period, yield rate, and deposit cap.

Product
Lock-up Period
Best For

trUSD-1M

30 days

Short-term yield seekers wanting monthly liquidity

trUSD-3M

90 days

Medium-term holders optimizing yield/liquidity

trUSD-6M

180 days

Capital allocators comfortable with semi-annual cycles

trUSD-1Y

365 days

Long-term DeFi participants maximizing yield

Note: All variants share the same contract code (RollingBond.sol) with different constructor parameters. The lock-up period and redemption window are set at deployment and cannot be changed β€” there are no setter functions for these parameters. They are fixed commitments built into the contract at construction time.


System Architecture

Contract Inheritance

Full System Diagram

Key Design Choice: No Token Custody

The vault never holds rUSD. On deposit, rUSD is burned from the user. On redemption, new rUSD is minted to the receiver. This means:

  • totalAssets() is a virtual accounting figure derived from shares Γ— yield factor

  • The protocol's actual rUSD supply is implicitly backed by the Reservoir balance sheet

  • There is no "vault balance" that can be drained β€” value is tracked purely via share accounting


Yield Accrual Mechanics

The Cumulative Rate Factor

Yield in RollingBond is not distributed as tokens. Instead, a single global variable β€” cumulativeRateFactor β€” tracks how much 1 share is worth in rUSD terms. It starts at 1.0 (represented as 1e27 in RAY precision) and only ever increases.

No additional shares are ever minted for yield. Share count stays constant; purchasing power increases.

Continuous Compounding Formula

The factor is updated using a 4-term binomial expansion approximation of per-second discrete compounding β€” (1 + r)^t β€” truncated to its first four terms:

Where:

  • r = per-second rate (in RAY format, i.e., Γ— 10²⁷)

  • t = seconds elapsed since last update

For practical per-second rates at any realistic time period, this binomial approximation is numerically indistinguishable from continuous compounding (e^(rΒ·t)) and is significantly more gas-efficient than an exact power function.

The factor is applied lazily β€” it is only computed when a state-changing operation occurs (deposit, request, complete, cancel, early redeem) or when view functions are called.

Rate History

Every time the MANAGER calls setRate(), a new entry is appended to the rateHistory array. This records the exact cumulative factor and timestamp when each rate became active. The _getFactorAt(timestamp) function walks this history to reconstruct the exact cumulative factor at any arbitrary past timestamp β€” enabling precise payout calculations even when the yield rate changed one or more times during a user's lock-up period.

RAY Precision

All rates and factors use RAY precision (1e27) β€” 27 decimal places β€” to prevent rounding loss during compounding:

Common Rates Reference

Target APY
Per-Second Rate (RAY format)
Notes

5%

1,546,000,000,000,000,000

Conservative

10%

3,020,000,000,000,000,000

Moderate

17%

4,976,000,000,000,000,000

50%

12,850,000,000,000,000,000

100%

21,960,000,000,000,000,000

1000%

76,040,000,000,000,000,000

Max practical (~857% actual due to truncation)

Compounding Approximation Precision: At very high rates (>1000% APY target), the 4-term binomial truncation underestimates the true result. At a 1000% APY target, actual realized yield is approximately 857%. This is documented and acceptable for the use case. Maximum allowed rate (MAX_RATE = 1e21) caps at approximately 3,150% APY.


User Flows

1. Deposit Flow

Depositing rUSD mints trUSD shares at the current share price (i.e., the current cumulativeRateFactor).

Example:

  • User deposits 1,000 rUSD when cumulativeRateFactor = 1.05 (5% has accrued since deploy)

  • Shares minted = 1,000 Γ· 1.05 = 952.38 trUSD

  • After another 30 days at 10% APY: shares worth 952.38 Γ— 1.0582 = ~1,008.40 rUSD


2. Standard Redemption Flow (Lock-up Path)

The standard path gives the value of shares at the moment the lock-up ends β€” with no fee β€” after waiting the lock-up period. Yield continues to accrue throughout the entire lock-up duration and stops only at unlockTime.

Timeline example (trUSD-1M at 10% APY):

Yield Freezes at Unlock Time: The payout is calculated using the cumulative rate factor at unlockTime β€” the moment the lock-up ends. Yield continues to accrue normally throughout the lock-up period. Once unlockTime is reached, the value is frozen and no additional yield accrues during the redemption window. Users who complete their redemption on day 75 or day 82 receive the same amount β€” the value the shares had exactly at unlockTime.

Redemption Window Expiry: If a user fails to call completeRedemption within the redemption window, the request expires. The locked shares are automatically returned when the user calls requestRedemption or redeemEarly again (auto-clear). No manual cancelRedemption is needed for expired requests.


3. Early Redemption Flow (Instant Path)

Users who need immediate liquidity can redeem at any time by paying the earlyRedemptionFee (configurable by MANAGER, default 5%).

Example (at 5% fee, 10% APY, Day 45):

minAssetsOut Slippage Protection: Pass minAssetsOut = 0 to disable slippage protection, or pass a calculated minimum to guard against fee changes or rate updates between transaction submission and execution.

Active Request Restriction: If a user has an active (non-expired) redemption request, they cannot call redeemEarly β€” even for shares not included in the request. They must cancel the pending request first. This is by design; expired requests are auto-cleared automatically.


4. Cancel Redemption

A user can cancel a pending redemption request at any time β€” during the lock-up period or during the redemption window.

After cancellation, shares resume accruing yield normally. The user can then re-request redemption at a higher share value.


5. Redemption State Machine


Smart Contract Interface

Constructor Parameters

Public User Functions

Function
Description

deposit(uint256 assets, address receiver)

Deposit rUSD, receive trUSD shares

requestRedemption(uint256 shares)

Start lock-up countdown; locks shares in vault

completeRedemption(address receiver)

Complete after lock-up, within window

cancelRedemption()

Cancel pending request, unlock shares

redeemEarly(uint256 shares, address receiver, uint256 minAssetsOut)

Instant redemption with fee

View / Preview Functions

Function
Returns

convertToAssets(uint256 shares)

Current rUSD value of shares

convertToShares(uint256 assets)

Shares received for given rUSD

totalAssets()

Total notional rUSD in vault (see note below)

getRedemptionRequest(address user)

(shares, requestTime, unlockTime, windowEnd, canRedeem)

previewCompleteRedemption(address user)

Assets at factor as of min(now, unlockTime)

previewRedeemEarly(uint256 shares)

(assetsAfterFee, feeAmount) at current factor

previewRedeem(uint256 shares)

Assets at current factor (not frozen)

apy()

Current annual yield in RAY format

getCurrentCumulativeFactor()

Current live cumulative factor

rateHistoryLength()

Number of rate-change intervals recorded

maxDeposit(address)

Remaining capacity under cap (0 = unlimited)

previewRedeem vs. previewCompleteRedemption: These return different values by design. previewRedeem(shares) answers "what is the current asset value of these shares?" (current factor, always growing). previewCompleteRedemption(user) answers "what will my existing pending request pay out?" β€” it uses _getFactorAt(min(now, unlockTime)): growing during the lock-up, then frozen at unlockTime once the window opens. DeFi integrators using the ERC4626 interface will see previewRedeem β€” the current-factor value.

totalAssets() Note: This function values all outstanding shares at the current live rate, including shares locked in active redemption requests. Locked shares will actually be paid out at their unlockTime factor (which is lower than the current factor if the rate has risen since their lock started). totalAssets() therefore slightly overestimates actual vault obligations. This is accepted behavior β€” it is a virtual accounting figure used for ERC4626 compatibility and cap enforcement.

Manager Functions (MANAGER role required)

Function
Description

setRate(uint256 newRate)

Update per-second yield rate (≀ MAX_RATE)

setCap(uint256 newCap)

Set maximum total assets (0 = unlimited)

setEarlyRedemptionFee(uint256 newFee)

Set early redemption fee (0–100%, in RAY)

recover(address token, address receiver)

Rescue accidentally sent non-rUSD tokens (transfers full balance of that token)

Events


Access Control

RollingBond uses OpenZeppelin's AccessControl with two roles:

Last updated