> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superearn.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Fixed-rate vault architecture and rate updates

> How fixed APR changes share value, how asset precision affects the update cadence, and how funds and keeper operations move through the vault.

The fixed-rate vault calculates **underlying asset value per share from the configured APR and elapsed time**. Interest does not increase the number of shares in your wallet. It increases the **rate** used to convert shares into assets. The default APR is **2% annually**. The operating target is a regular update **every 24 hours for USDT (6 decimals) and every hour for JPYC (18 decimals)**.

This page explains the architecture and operating model. For addresses, approval targets, function calls, and testing, use the [fixed-rate vault integration guide](/en/developers/integrate-fixed-rate-vault.md).

> [!NOTE]
>
> These intervals are **operating targets for the off-chain keeper**. Contracts do not automatically select a cadence from decimals or execute themselves when a timer expires: the confirmed rate changes only after a successful keeper `poke` transaction. Kairos testnet has no automated keeper, so coordinate rate-update and redemption tests with the SuperEarn team.

## Funds and contract architecture

Users call Router to deposit and submit withdrawal requests to Queue. The path that invests assets is separate from the path that determines the rate applied to user shares.

```mermaid theme={null}
flowchart TB
    U[User] -->|Deposit underlying| R[VedaRouter]
    R -->|Deposited assets| V[BoringVault]
    V -->|Mint shares| U
    V -->|Allocate assets| G[Gateway]
    G -->|Deposit| C[CustomVault]
    C -->|Allocate| S[Registered strategies]
    S -->|Recall| C
    C -->|Pay recalled assets directly| V
```

The diagram summarizes asset movement. Router calls Teller, which instructs Vault to mint shares. The keeper moves and recalls investment assets through Gateway. Gateway holds CustomVault shares and temporarily holds underlying during deposits. Normal CustomVault withdrawals pay underlying directly back to Vault.

| Role | Contract | Responsibility |
| - | - | - |
| User deposits | VedaRouter | Receive underlying and apply minimum shares, cap, whitelist, and other deposit policies |
| Custody and share token | BoringVault (`VAULT`) | Hold assets and mint/burn users' 18-decimal shares |
| Confirmed rate | Accountant | Store the last accepted rate and provide it to Teller |
| APR calculation | FixedAprRateController | Calculate the next rate from configured APR and elapsed time |
| Minting and redemption | Teller | Read the confirmed Accountant rate to mint/redeem shares |
| Withdrawal requests and settlement | AtomicQueue / AtomicSolverV4 | Store requests, then redeem shares and pay assets within one settlement transaction |
| Operations entrypoint | VedaKeeper | Execute authorized rate updates, liquidity movements, and withdrawal settlements |
| Investment allocation | Gateway / CustomVault / strategies | Allocate Vault assets to registered strategies and recall withdrawal liquidity |

The user deposit call path is `VedaRouter → Teller → Vault`. The rate-update path is `VedaKeeper.poke → Controller.poke → Accountant.updateExchangeRate`. Manager and role configuration constrain the calls allowed to move Vault assets.

The fixed rate does not copy strategy NAV (net asset value). Controller follows the configured APR formula even when strategy returns change. The operator must manage assets and liquidity to support redemptions at that rate. **Increasing the rate does not itself create tokens or investment profits.**

## How fixed APR is applied

The target APY is 2% (governance-configurable). The rate follows a fixed curve anchored to absolute time, so a keeper `poke` sets:

```text theme={null}
newRate = floor(anchorRate × ratePerSecond^(now - anchorTime))
```

* `anchorRate`, `anchorTime`: the fixed reference point (last rate change or deployment), not the previous rate.
* `ratePerSecond`: a per-second compounding factor (27-decimal fixed point) derived from the target APY, where `ratePerSecond^(365 days) = 1 + APY`. Computed with integer exponentiation (`rpow`); fractional rate units are discarded (rounded down).
* `now - anchorTime`: absolute seconds since the anchor — the only time input.
* `lastPokeAt`: the last successful push timestamp; it gates the keeper's minimum interval but does not change the rate value.

Because the rate depends only on absolute elapsed time, **poke cadence no longer changes the realized APY**: a late or irregular `poke` lands the confirmed rate on the exact same curve, with no drift. `aprBps()` / `targetApyBps()` read the effective APY back on-chain (2%).

The rate is expressed in **base units of Accountant's base asset**. `Accountant.decimals()` equals the base token's decimals. A JPYC-based vault's rate has **18 decimals**; a USDT-based vault's rate has **6 decimals**. This is separate from both vaults having 18-decimal shares.

| Base asset | Rate decimals | Rate when 1 share = 1 asset |
| - | - | - |
| USDT | 6 | `1_000_000` |
| JPYC | 18 | `1_000_000_000_000_000_000` |

In both cases, convert a share amount to asset value using `floor(shares × rate / 10^18)`. The result is in the corresponding base asset's smallest units.

The default target APY is 2%, but governance can change it via `setRatePerSecond`. If time has elapsed, it first settles that period at the old rate, then re-anchors and applies the new rate prospectively, so a change never retro-applies. Governance changes can therefore update the rate outside the regular schedule. **Realized annual growth equals the configured APY regardless of the poke schedule**; integer rounding only shaves sub-unit amounts at the floor of the rate.

## How stepped updates differ from PPS

In the existing variable-yield vault, PPS is calculated from free funds and share supply at the observation time. After strategy profits are reported, the calculation reflects profit unlocking based on `lastReport` and `lockedProfitDegradation`. PPS can therefore change with time without another report transaction. The unlock duration is configurable. This does **not** mean every unreported strategy profit is immediately recognized. See [valuation, harvest and accrual reporting](/en/developers/accrual-accounting.md).

In the fixed-rate vault, `Accountant.getRate()` returns a **stored number**. Time passing, a read, a deposit, or a withdrawal request does not itself increase that number. A successful scheduled `poke` recognizes the elapsed period in a single update.

| Property | Existing variable-yield vault | Fixed-rate vault |
| - | - | - |
| Valuation basis | PPS reflecting reported assets, gains/losses, and profit unlocking | Rate updated from configured APR and elapsed time |
| Reads between updates | PPS can change as profits unlock | `getRate()` stays the same until the next successful checkpoint |
| Typical display | Gradual change while profits unlock; reports and losses also affect PPS | Steps at the vault's configured operating cadence |
| Time-based projection | Read the time-dependent components of the vault's PPS calculation | Read `previewNextRate()` separately; it is not the confirmed transaction rate |

![The confirmed rate remains flat from 0 to 24 hours and rises at the 24-hour and 48-hour checkpoints. A separate projected rate grows between checkpoints.](https://mintcdn.com/superearn-fe42391a/AbaTcACQ_l5cbBHR/images/fixed-rate-daily-checkpoints.svg?fit=max\&auto=format\&n=AbaTcACQ_l5cbBHR\&q=85\&s=8fd0b80cb0c3defa5934f20530720173)

The solid line is the confirmed rate; the dashed line is the projected rate if poked at that time. This chart and the table below use 2% APR, a 6-decimal asset, an initial rate of 1,000,000, and a 24-hour cadence. JPYC uses a shorter update cadence.

| Time | Confirmed rate | Confirmed asset value of 1 share | Action |
| - | - | - | - |
| Start | 1,000,000 | 1.000000 | Initial value |
| 12 hours | 1,000,000 | 1.000000 | Reading does not update the rate |
| 24 hours, after successful poke | 1,000,054 | 1.000054 | First update |
| 36 hours | 1,000,054 | 1.000054 | Unchanged until the next update |
| 48 hours, after successful poke | 1,000,108 | 1.000108 | Second update |

This is **not separate interest accounting from each user's deposit timestamp**. A deposit uses the current confirmed rate, and Router does not automatically poke. The next checkpoint applies to all shares, including shares deposited shortly before that update. Do not present interest prorated to an individual user's holding time as identical to confirmed contract value.

Create new requests with `safeUpdateAtomicRequest`, the official Accountant, and `discount=0`. It reads and stores the confirmed rate at request execution; the call does not itself poke. Cancel using ordinary `updateAtomicRequest` with a zero amount.

A withdrawal request also stores its own `atomicPrice`. A later rate increase does not automatically increase an existing request's payout. To request at a new rate, cancel the unwanted existing request and submit another using the latest confirmed rate.

## Default keeper operating rules

**Use an update cadence appropriate to the base asset's precision.** These intervals are not withdrawal waiting periods or settlement guarantees: rate updates and withdrawal settlement are separate jobs.

| Deployment | Default operating cadence | Reason or exception |
| - | - | - |
| USDT-based vault — 6 decimals | **Every 24 hours** | Gas and a visible daily step; cadence does not change the APY (the rate is anchored to absolute time) |
| JPYC-based vault — 18 decimals | **Every hour** | Higher precision keeps each confirmed step smooth; hourly balances freshness against gas |
| Kairos mockUSDT testnet | **No automated keeper** | Coordinate manual poke and settlement tests with the team |

| Task | Default rule |
| - | - |
| Regular rate update | An authorized operator calls `VedaKeeper.poke()` about every 24 hours for USDT or every hour for JPYC, measured from the last successful checkpoint |
| Before execution | Check Controller `minInterval`, Accountant delay and rate-change bounds, relevant pauses, and `previewNextRate` |
| Confirm execution | Verify the successful receipt, `Poked` / `ExchangeRateUpdated`, and the updated `getRate()` and `lastPokeAt()` |
| Delays and failures | Diagnose and retry. The formula uses elapsed time; missed intervals do not create separate accrual records |
| Liquidity management | Prepare redemption assets and recall funds through Gateway/CustomVault into Vault when necessary |
| Withdrawal settlement | Check each request's deadline, shares, allowance, fixed price, pauses, and liquidity before separately calling `redeemSolve` |

The hourly and 24-hour cadences are operating schedules; the current contracts do not automatically enforce updates at those intervals. `minInterval` sets a minimum allowed spacing, not an automatic timer. **A shorter cadence requires both Controller's `minInterval` and Accountant's `minimumUpdateDelayInSeconds` to allow updates at that frequency.** Higher precision does not bypass those restrictions, pauses, or rate-change bounds. Confirm a price change from a successful on-chain checkpoint, not the timetable. The schedule does not promise execution at a particular UTC time or withdrawal completion within that interval.

If a poke violates Accountant bounds or pause conditions, the whole transaction reverts and both the rate and checkpoint timestamp remain unchanged. After prolonged downtime, accumulated growth may exceed the allowed change and require operator attention. Do not display a failed update as recognized interest.

Settlement does not automatically poke. To test a redemption at an updated rate, use **successful poke → read latest rate → user submits request → settlement**. Existing requests can still settle at their own fixed prices; waiting for a rate update does not protect them from settlement.

### USDT: 6 decimals and a 24-hour cadence

The confirmed rate is a function of absolute elapsed time, so **poke cadence does not change the realized APY** — poking hourly, every 24 hours, or catching up after a long gap all land the rate on the same curve. The table uses an initial rate of `1_000_000`, a constant 2% target APY, and 365 days without changes.

| Poke interval | Rate increase at first poke | Growth after 365 days |
| - | - | - |
| 1 hour | 2 | ≈ 2.000% |
| **24 hours — USDT default cadence** | **54** | **≈ 2.000%** |
| 48 hours | 109 | ≈ 2.000% |

Every cadence reaches the same confirmed rate (here `1_000_000 → 1_019_999`); the "first poke" column differs only in step size, not in the annual total. At this 6-decimal scale, integer rounding is a one-time sub-unit floor at each read, not a cadence-dependent loss: a one-unit increase first appears about **1,593 seconds** after the anchor. A successful poke advances `lastPokeAt` even when the rounded rate does not increase. The 24-hour default is chosen for gas and a visible daily step, not to protect the APY.

### JPYC: 18 decimals and an hourly cadence

For JPYC with a rate near `10^18`, the sub-unit discarded at each read is below `10^-18` JPYC per share, so short intervals lose effectively nothing. At `10^18` and a 2% target APY, the first update after exactly one hour adds `2_260_576_447_357` rate units (**`0.000002260576447357` JPYC per share**). The default operating target is **one poke per hour**; even 10-minute intervals are feasible precision-wise, but update restrictions, gas, and operating costs still apply.

Because the curve is anchored to absolute time, updating hourly, every 10 minutes, or after a gap all yield the **same \~2.000% over 365 days** — cadence does not compound differently and does not drift from the target.

More frequent updates still leave `getRate()` unchanged between successful pokes. JPYC has a **stepwise confirmed price targeting hourly updates**, not a continuously changing one. Existing withdrawal requests' `atomicPrice` values do not update automatically either.

## What integrations should display and test

* **Confirmed rate and update time:** display `getRate()` with `lastPokeAt()`. Connect actual checkpoints with horizontal steps on charts.
* **Projected current rate:** label `previewNextRate()` separately as a projection. Do not substitute it for confirmed balance value, deposit quotes, or completed payouts.
* **Expected next scheduled update:** add the vault's operating cadence (24 hours for USDT, one hour for JPYC) to the last checkpoint and label it as an estimate. Maintain this cadence as deployment-specific operational configuration; do not infer automatic execution from decimals or `minInterval` alone. Do not advance the confirmed rate merely because that time has arrived. Do not show a scheduled next update on Kairos, where there is no automated keeper.
* **Delayed updates and pending withdrawals:** if the cadence is exceeded, show the last successful update and its delayed status. Define the app's acceptable quote age separately and coordinate stale-rate handling with the team. Distinguish request confirmation, pending settlement, and completed payment.

On Kairos, users can mint mockUSDT, deposit, submit requests, and read state themselves. Coordinate rate-update or redemption tests through [contact@superearn.io](mailto:contact@superearn.io), following the [testnet integration instructions](/en/developers/integrate-fixed-rate-vault.md#7-test-on-kairos). Mainnet addresses will be added to the integration guide after the production deployment.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.