# Fixed Lending - trUSD (Rolling Bond)

> **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**

```
RollingBond
├── AccessControl (OpenZeppelin)   — Role-based permissions
├── ERC20 (OpenZeppelin)           — trUSD share token
├── ERC4626 (OpenZeppelin)         — Standard vault interface
└── ReentrancyGuard (OpenZeppelin) — Reentrancy protection
```

**Full System Diagram**

```
                        ┌─────────────────────────────────┐
                        │        Access Control           │
                        │                                 │
                        │  DEFAULT_ADMIN ─────────────┐   │
                        │  (multisig)    grants roles │   │
                        │                             ▼   │
                        │  MANAGER ───────────────────────────►  setRate()
                        │  (multisig)                     │      setCap()
                        └─────────────────────────────────┘      setEarlyRedemptionFee()
                                                         
                                                         
  ┌───────────────────────────────────────────────────────────────────────┐
  │                         USER ACTIONS                                  │
  │                                                                       │
  │   deposit(assets)          requestRedemption(shares)                  │
  │   ─────────────            ─────────────────────────                  │
  │   Burn rUSD from user      Lock shares in vault                       │
  │   Mint trUSD shares        Record unlockTime                          │
  │   to receiver              Yield continues to accrue                  │
  │         │                         │                                   │
  │         │                         │  [wait lock-up period]            │
  │         │                         │  [yield accrues until unlockTime] │
  │         │                         │                                   │
  │         │                         ▼                                   │
  │         │              completeRedemption(receiver)                   │
  │         │              ─────────────────────────────                  │
  │         │              Must be within redemption window               │
  │         │              Burn locked shares                             │
  │         │              Mint rUSD to receiver                          │
  │         │              (payout = value at unlockTime)                 │
  │         │                                                             │
  │         │   redeemEarly(shares, receiver, minOut)                     │
  │         │   ─────────────────────────────────────                     │
  │         │   No waiting required                                       │
  │         │   Burn shares immediately                                   │
  │         │   Mint rUSD minus early fee to receiver                     │
  │         │                                                             │
  └─────────┼─────────────────────────────────────────────────────────────┘
            │
            ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                    RollingBond (ERC4626)                        │
  │                                                                 │
  │   cumulativeRateFactor  ─── Tracks total yield growth           │
  │   (starts at 1.0, only ever increases)                          │
  │                                                                 │
  │   rateHistory[]  ─── Records each rate-change interval          │
  │   (used to reconstruct the factor at any past timestamp)        │
  │                                                                 │
  │   convertToAssets(shares) = shares × cumulativeRateFactor       │
  │   convertToShares(assets) = assets ÷ cumulativeRateFactor       │
  │                                                                 │
  │   totalSupply()  = total trUSD shares outstanding               │
  │   totalAssets()  = totalSupply × cumulativeRateFactor           │
  └──────────────────────────────┬──────────────────────────────────┘
                                  │
                                  ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                     rUSD (IStablecoin)                          │
  │                                                                 │
  │   burnFrom(user, amount)  ← called on deposit                   │
  │   mint(receiver, amount)  ← called on redemption                │
  │                                                                 │
  │   RollingBond must hold MINTER role on asset contract           │
  └─────────────────────────────────────────────────────────────────┘
```

**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.

```
cumulativeRateFactor over time (at 10% APY example):

Day   0:  1.000000  (1:1, deposit 100 rUSD → 100 trUSD shares)
Day  30:  1.008214  (100 shares now worth 100.82 rUSD)
Day  90:  1.024684  (100 shares now worth 102.47 rUSD)
Day 180:  1.049964  (100 shares now worth 104.99 rUSD)
Day 365:  1.100000  (100 shares now worth 110.00 rUSD)
```

**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:

```
(1 + r)^t ≈ 1 + t·r + t(t-1)/2 · r² + t(t-1)(t-2)/6 · r³
```

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:

```solidity
uint256 public constant RAY = 1e27;

// Share → Asset conversion:
assets = mulDiv(shares, cumulativeRateFactor, RAY)

// Asset → Share conversion:
shares = mulDiv(assets, RAY, cumulativeRateFactor)
```

**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`).

```
User holds rUSD
      │
      │  1. user.approve(rollingBond, amount)
      ▼
RollingBond.deposit(amount, receiver)
      │
      │  2. Update cumulativeRateFactor to now
      │  3. Calculate shares = amount ÷ currentFactor
      │  4. IStablecoin(rUSD).burnFrom(caller, amount)   ← rUSD destroyed
      │  5. _mint(receiver, shares)                       ← trUSD created
      ▼
Receiver now holds trUSD shares
(value accrues second-by-second going forward)
```

**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`.

```
Phase 1: REQUEST
─────────────────
User calls requestRedemption(shares)
      │
      ├── Checks: no existing active request, balance ≥ shares
      ├── Calculates unlockTime = block.timestamp + lockupPeriod
      ├── Transfers shares to vault (locked)                         ← SHARES LOCKED HERE
      └── Emits RedemptionRequested event

Phase 2: WAIT (lock-up period)
───────────────────────────────
[30 / 90 / 180 / 365 days pass]

The global cumulativeRateFactor continues to grow for ALL depositors,
including this user's locked shares. Yield accrues right up to unlockTime.

Phase 3: COMPLETE (within redemption window)
────────────────────────────────────────────
User calls completeRedemption(receiver)
      │
      ├── Checks: block.timestamp ≥ unlockTime
      ├── Checks: block.timestamp ≤ unlockTime + REDEMPTION_WINDOW
      ├── Calculates factorAtUnlock = _getFactorAt(unlockTime)       ← YIELD FROZEN AT UNLOCK TIME
      ├── Calculates assets = shares × factorAtUnlock ÷ RAY
      ├── Burns locked shares from vault
      ├── IStablecoin(rUSD).mint(receiver, assets)   ← rUSD created
      └── Emits RedemptionCompleted event
```

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

```
Day 0    ──────────────────────────────────────────── Deposit $1,000
                                                       Receive 1,000 trUSD

Day 45   ──────────────────────────────────────────── requestRedemption(1000)
           Current value: $1,012.36                   Lock-up starts (30 days)
           Shares transferred to vault                Yield continues to accrue

Day 45-75  [Yield accrues continuously for all holders, including locked shares]

Day 75   ──────────────────────────────────────────── Lock-up ends, window opens
           Value at unlock: $1,020.82                 YIELD FROZEN AT $1,020.82
           Window: Day 75 → Day 82 (7 days)

Day 78   ──────────────────────────────────────────── completeRedemption(wallet)
           Receive: $1,020.82  (factor captured at Day 75 unlock time)

Day 83+  ──────────────────────────────────────────── Window expired
           Must submit new requestRedemption
```

> **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%).

```
User calls redeemEarly(shares, receiver, minAssetsOut)
      │
      ├── Checks: no active pending request (or auto-clears if expired)
      ├── Checks: balance ≥ shares
      ├── Updates cumulativeRateFactor to now
      ├── Calculates assetsBeforeFee = shares × currentFactor ÷ RAY
      ├── Calculates fee = assetsBeforeFee × earlyRedemptionFee ÷ RAY
      ├── Calculates assets = assetsBeforeFee - fee
      ├── Checks: assets ≥ minAssetsOut (slippage protection)
      ├── Burns shares from user
      ├── IStablecoin(rUSD).mint(receiver, assets)
      └── Emits EarlyRedemption event
```

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

```
Deposit Day 0:  1,000 rUSD → 1,000 trUSD shares

Day 45 early redeem:
  Current value:   $1,012.36
  Fee (5%):         - $50.62
  ─────────────────────────
  Received:         $961.74  (immediate, no waiting)
```

> **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.

```
User calls cancelRedemption()
      │
      ├── Checks: pending request exists
      ├── Deletes redemptionRequests[msg.sender]
      ├── Transfers locked shares back to user
      └── Emits RedemptionCancelled event

Shares return to user's wallet and resume yield accrual from the global factor.
```

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

***

**5. Redemption State Machine**

```
                    ┌──────────┐
        deposit()   │          │
   ────────────────►│  ACTIVE  │◄──────────────────────────────┐
                    │ (holding)│                               │
                    └────┬─────┘                               │
                         │                                     │
              requestRedemption(shares)                        │
                         │                                     │
                         ▼                                     │
                    ┌──────────┐                               │
                    │  LOCKED  │  [yield accrues → unlockTime] │
                    │(in vault)│                               │
                    └────┬─────┘                               │
                         │                                     │
               ┌─────────┴─────────┐                           │
               │                   │                           │
        cancelRedemption()    [lockupPeriod elapses]           │
               │                   │                           │
               │              ┌────▼──────┐                    │
               │              │  WINDOW   │ [value frozen at   │
               │              │  OPEN     │  unlockTime]       │
               │              └────┬──────┘                    │
               │                   │                           │
               │       ┌───────────┴──────────┐                │
               │       │                      │                │
               │ completeRedemption()   [window expires]       │
               │       │                      │                │
               │       ▼                      │                │
               │  ┌─────────┐          AUTO-CLEAR              │
               │  │ REDEEMED│          on next call            │
               │  │  (done) │                 │                │
               │  └─────────┘                 └────────────────┘
               │
               └─────────────────────────────────────────────►
                                  back to ACTIVE
```

***

#### Smart Contract Interface

**Constructor Parameters**

```solidity
constructor(
    address admin,                  // DEFAULT_ADMIN_ROLE holder (use multisig)
    string memory name,             // Token name, e.g. "trUSD-1M"
    string memory symbol,           // Token symbol, e.g. "trUSD-1M"
    IERC20Metadata asset,           // rUSD contract address
    uint256 _lockupPeriod,          // Lock-up in seconds (max: 365 days)
    uint256 _redemptionWindow,      // Window in seconds (production: 7 days = 604800)
    uint256 initialRate,            // Per-second yield rate in RAY format
    uint256 initialEarlyRedemptionFee // Fee in RAY (e.g. 0.05e27 = 5%)
)
```

**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**

```solidity
event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
event RedemptionRequested(address indexed user, uint256 shares, uint256 requestTime, uint256 unlockTime);
event RedemptionCompleted(address indexed user, address indexed receiver, uint256 shares, uint256 assets);
event RedemptionCancelled(address indexed user, uint256 shares);
event EarlyRedemption(address indexed user, address indexed receiver, uint256 shares, uint256 assets, uint256 fee);
event RateUpdated(uint256 oldRate, uint256 newRate, uint256 timestamp);
event CapUpdated(uint256 oldCap, uint256 newCap);
event EarlyRedemptionFeeUpdated(uint256 oldFee, uint256 newFee);
```

***

#### Access Control

RollingBond uses OpenZeppelin's `AccessControl` with two roles:

```
DEFAULT_ADMIN_ROLE (bytes32(0))
├── Granted to: admin address in constructor
├── Can: grant and revoke any role (including MANAGER)
└── Cannot: directly set rates, caps, fees, or access user funds

MANAGER (keccak256(abi.encode("rollingbond.manager")))
├── Can: setRate, setCap, setEarlyRedemptionFee, recover
└── Cannot: mint shares, modify pending redemption requests, transfer user funds
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.reservoir.xyz/products/fixed-lending-trusd-rolling-bond.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
