> For the complete documentation index, see [llms.txt](https://docs.reservoir.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.reservoir.xyz/products/term-trusd-rolling-bond.md).

# Term - 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   │   │  wsrUSD  │   │  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
```
