--- eip: 8416 title: Epoch-Based Fixed-Rate Vault description: An ERC-4626 vault that progresses through a sequence of fixed-rate epochs author: Marcelo Morgado (@marcelomorgado), Manoj Patidar (@patidarmanoj10), Rohit Solia (@rokso) discussions-to: https://ethereum-magicians.org/t/erc-8416-epoch-based-fixed-rate-vault/29669 status: Draft type: Standards Track category: ERC created: 2026-06-26 requires: 20, 165, 4626, 7540, 7575 --- ## Abstract The following standard extends [ERC-4626](./eip-4626.md) with a vault that progresses through a sequence of fixed-rate epochs. Each epoch has its own immutable rate and maturity. Each epoch exposes three timing intervals, an entry window, an accrual interval, and an exit window, which MAY coincide or run in sequence. Deposits and redemptions are each, at the implementer's choice, synchronous (plain [ERC-4626](./eip-4626.md)) or asynchronous ([ERC-7540](./eip-7540.md) request, fulfill, and claim). Each chosen variant is discoverable via [ERC-165](./eip-165.md). Shares stay fungible across epochs because the vault rolls over the share-to-asset price. ## Motivation Because [ERC-4626](./eip-4626.md) derives its exchange rate from total assets divided by total supply, the rate moves with strategy performance and cannot represent a fixed term and rate. Fixed-income products therefore need laddered maturities instead of a single term. This standard carries one share token across an indefinite sequence of fixed-rate terms, so an integrator lists the vault once. It composes existing primitives: [ERC-4626](./eip-4626.md) for share-price mechanics and for gated entry and exit, and [ERC-7540](./eip-7540.md) for the optional request, fulfill, and claim lifecycle on either leg. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. ### Definitions The existing definitions from [ERC-4626](./eip-4626.md) and [ERC-7540](./eip-7540.md) apply. In addition, this spec defines: - **Epoch**: one fixed-rate term, spanning its entry window, accrual interval, and exit window. Its rate, accrual interval, and deposit capacity are immutable. - **Epoch period**: the full span covering the epoch's intervals. - **Entry window**: the interval during which deposits are permitted. It opens at the beginning of the epoch and MAY span the entire epoch period. - **Accrual interval**: the interval over which interest accrues. It MAY span the entire epoch period. - **Exit window**: the interval during which redemptions are permitted. It closes at the end of the epoch and MAY span the entire epoch period. - **Maturity**: the instant the accrual interval ends and interest stops accruing. - **Fulfillment**: in the asynchronous variants, the resolution of a pending request, after which it becomes claimable. Its timing is at the implementation's discretion. See the [Request fulfillment](#request-fulfillment) section. - **Limbo**: the gap that MAY exist between two epochs, during which no epoch is current. - **Terminated**: the state of a vault that has been permanently wound down. See the [Termination](#termination) section. Every interval in this standard is half-open, `[start, end)`: a timestamp `t` is inside the interval if and only if `start <= t < end`. ### Conformance A conformant vault MUST implement [ERC-4626](./eip-4626.md), [ERC-20](./eip-20.md), [ERC-165](./eip-165.md), and the base interface `IERC8416` defined in the [Interfaces](#interfaces) section. A conformant vault MUST implement exactly one deposit variant and exactly one redemption variant, each independently discoverable via `supportsInterface`. A leg is synchronous when the base identifier is present and the corresponding [ERC-7540](./eip-7540.md) identifier is absent. Deposit variants: - Synchronous-deposit. Deposits are atomic [ERC-4626](./eip-4626.md) `deposit` and `mint` calls. The vault MUST NOT implement `IERC7540Deposit`. - Asynchronous-deposit. Deposits follow the [ERC-7540](./eip-7540.md) request, fulfillment, and claim lifecycle. The vault MUST implement both `IERC8416AsyncDeposit` and `IERC7540Deposit`. Redemption variants: - Synchronous-redeem. Redemptions are atomic [ERC-4626](./eip-4626.md) `redeem` and `withdraw` calls. The vault MUST NOT implement `IERC7540Redeem`. - Asynchronous-redeem. Redemptions follow the [ERC-7540](./eip-7540.md) request, fulfillment, and claim lifecycle. The vault MUST implement both `IERC8416AsyncRedeem` and `IERC7540Redeem`. The two axes are independent: a vault MAY combine any deposit variant with any redemption variant. ### ERC-165 support Smart contracts implementing this Vault standard MUST implement the [ERC-165](./eip-165.md) `supportsInterface` function. All vaults implementing this standard MUST return the constant value `true` if the identifier of the base interface `IERC8416` is passed through the `interfaceID` argument. Vaults implementing the asynchronous-deposit variant MUST return the constant value `true` if `0xce3bbe50` (the [ERC-7540](./eip-7540.md) asynchronous deposit identifier) is passed through the `interfaceID` argument. Vaults implementing the synchronous-deposit variant MUST return `false` for that argument. Vaults implementing the asynchronous-redeem variant MUST return the constant value `true` if `0x620ee8e4` (the [ERC-7540](./eip-7540.md) asynchronous redemption identifier) is passed through the `interfaceID` argument. Vaults implementing the synchronous-redeem variant MUST return `false` for that argument. | Interface | ERC-165 identifier | `true` when | | ----------------- | ---------------------------------------- | --------------------------------------------------------------------- | | `IERC8416` (base) | `type(IERC8416).interfaceId` | Always, for every conformant vault | | `IERC7540Deposit` | `0xce3bbe50` | Asynchronous-deposit variant | | `IERC7540Redeem` | `0x620ee8e4` | Asynchronous-redeem variant | | `IERC7575` | the [ERC-7575](./eip-7575.md) identifier | Either asynchronous variant, as required by [ERC-7540](./eip-7540.md) | The marker interfaces `IERC8416AsyncDeposit` and `IERC8416AsyncRedeem` do not define separate identifiers. They are implied by the identifiers above, provided the base identifier is present: `0xce3bbe50` then denotes `IERC8416AsyncDeposit`, this standard's superset of `IERC7540Deposit`. Likewise, `0x620ee8e4` denotes `IERC8416AsyncRedeem`, its superset of `IERC7540Redeem`. A caller that observes the base identifier alongside either [ERC-7540](./eip-7540.md) identifier MAY therefore rely on the corresponding [maxRequestDeposit](#maxrequestdeposit) or [maxRequestRedeem](#maxrequestredeem) method being present. ### Rate semantics `rate(id)` is a simple annualized rate, WAD-scaled, on an ACT/365 basis, where `1e18` denotes 100% per annum. Interest accrues linearly over the accrual interval. For an amount `a` held from `t0` to `t1`, where `accrualInterval(id).start <= t0 <= t1 <= accrualInterval(id).end`, accrued interest MUST converge to `a * rate(id) * (t1 - t0) / (365 days * 1e18)`, where `365 days == 31_536_000`. Leap years and leap seconds are not accounted for. ### Methods The accessors taking an `id` accept any value. For an `id` that does not correspond to a created epoch, including `0`, they MUST return zeroed values rather than revert. #### epochId The index of the current epoch. Epoch indices begin at `1`, and `0` means no epoch has started yet. MUST return the greatest created `id` such that `epochPeriod(id).start <= block.timestamp`, and `0` if no such epoch exists. MUST NOT revert. ```yaml - name: epochId type: function stateMutability: view inputs: [] outputs: - name: id type: uint256 ``` #### rate The fixed rate of the epoch with the given `id`. See the [Rate semantics](#rate-semantics) section. MUST NOT revert. MUST be immutable once the epoch has been created. ```yaml - name: rate type: function stateMutability: view inputs: - name: id type: uint256 outputs: - name: rate type: uint256 ``` #### maxEpochDeposits The deposit capacity of the epoch with the given `id`, the maximum cumulative assets that MAY be deposited over its entry window. The value `type(uint256).max` denotes an uncapped epoch. MUST NOT revert. MUST be immutable once the epoch has been created. ```yaml - name: maxEpochDeposits type: function stateMutability: view inputs: - name: id type: uint256 outputs: - name: maxAssets type: uint256 ``` #### epochPeriod The active span of the epoch with the given `id`. MUST NOT revert. MUST NOT overlap an adjacent epoch: for any two created epochs `id` and `id + 1`, `epochPeriod(id).end <= epochPeriod(id + 1).start`. MUST be immutable once the epoch has been created. ```yaml - name: epochPeriod type: function stateMutability: view inputs: - name: id type: uint256 outputs: - name: start type: uint256 - name: end type: uint256 ``` #### entryWindow The interval of the epoch during which a deposit (synchronous variant) or a deposit request (asynchronous variant) is allowed. MUST NOT revert. MUST satisfy `start == epochPeriod(id).start` and `end <= epochPeriod(id).end`. MUST be immutable once the epoch has been created. ```yaml - name: entryWindow type: function stateMutability: view inputs: - name: id type: uint256 outputs: - name: start type: uint256 - name: end type: uint256 ``` #### accrualInterval The interval of `epochPeriod(id)` over which interest accrues. MUST NOT revert. MUST satisfy `epochPeriod(id).start <= start` and `end <= epochPeriod(id).end`. MUST be immutable once the epoch has been created. ```yaml - name: accrualInterval type: function stateMutability: view inputs: - name: id type: uint256 outputs: - name: start type: uint256 - name: end type: uint256 ``` #### exitWindow The interval of `epochPeriod(id)` during which a redemption (synchronous variant) or a redemption request (asynchronous variant) is allowed. MUST NOT revert. MUST satisfy `start >= epochPeriod(id).start` and `end == epochPeriod(id).end`. MUST be immutable once the epoch has been created. ```yaml - name: exitWindow type: function stateMutability: view inputs: - name: id type: uint256 outputs: - name: start type: uint256 - name: end type: uint256 ``` #### maxRequestDeposit The maximum assets that can be requested for the `owner` through a `requestDeposit` call, the asynchronous analog of `maxDeposit`. This method is part of the asynchronous-deposit variant only. MUST NOT revert. MUST return `0` whenever a deposit request would revert, for example outside the entry window, during limbo, or after termination. MUST NOT exceed the epoch's remaining deposit capacity. See the [maxEpochDeposits](#maxepochdeposits) section. ```yaml - name: maxRequestDeposit type: function stateMutability: view inputs: - name: owner type: address outputs: - name: maxAssets type: uint256 ``` #### maxRequestRedeem The maximum shares that can be requested for redemption for the `owner` through a `requestRedeem` call, the asynchronous analog of `maxRedeem`. This method is part of the asynchronous-redeem variant only. MUST NOT revert. MUST return `0` whenever a redemption request would revert, for example outside `exitWindow(epochId())` while the vault has not terminated. MUST NOT exceed the share balance of `owner`. ```yaml - name: maxRequestRedeem type: function stateMutability: view inputs: - name: owner type: address outputs: - name: maxShares type: uint256 ``` #### currentRate The rate currently accruing. MUST NOT revert. MUST return `0` when `block.timestamp` is outside an accrual interval. ```yaml - name: currentRate type: function stateMutability: view inputs: [] outputs: - name: rate type: uint256 ``` #### isTerminated Whether the vault has been permanently wound down. See the [Termination](#termination) section. MUST NOT revert. Once `true`, MUST remain `true`. ```yaml - name: isTerminated type: function stateMutability: view inputs: [] outputs: - name: terminated type: bool ``` ### Inherited method behavior This standard constrains [ERC-4626](./eip-4626.md) methods, and, in the asynchronous variants, [ERC-7540](./eip-7540.md) methods, as follows. All other [ERC-4626](./eip-4626.md) and [ERC-7540](./eip-7540.md) semantics are unchanged. #### deposit, mint In the synchronous-deposit variant, these MUST revert when `block.timestamp` is outside `entryWindow(epochId())`. In the asynchronous-deposit variant, these are the [ERC-7540](./eip-7540.md) deposit claim functions and MUST follow [ERC-7540](./eip-7540.md) semantics, operating only on the claimable balance. #### maxDeposit, maxMint In the synchronous-deposit variant, these MUST return `0` whenever a deposit would revert, for example outside the entry window, during limbo, or after termination, and otherwise MUST NOT exceed the epoch's remaining deposit capacity. See the [maxEpochDeposits](#maxepochdeposits) section. In the asynchronous-deposit variant, these MUST follow [ERC-7540](./eip-7540.md) semantics, reflecting the claimable balance. The deposit capacity gates new capital through [maxRequestDeposit](#maxrequestdeposit), not through `maxDeposit`. #### requestDeposit This method is part of the asynchronous-deposit variant only. MUST revert when `block.timestamp` is outside `entryWindow(epochId())` or when `isTerminated()` returns `true`. MUST revert when the requested assets exceed [maxRequestDeposit](#maxrequestdeposit) for `owner`. #### previewDeposit, previewMint In the asynchronous-deposit variant, these MUST revert, as required by [ERC-7540](./eip-7540.md) for an asynchronous deposit Vault. #### totalAssets SHOULD include interest accrued over the current epoch's accrual interval, so that the share price rises continuously. Accrual stops at maturity, so the price plateaus until the next accrual interval. #### redeem, withdraw In the synchronous-redeem variant, these MUST revert when `block.timestamp` is outside `exitWindow(epochId())`, except after termination. In the asynchronous-redeem variant, these are the [ERC-7540](./eip-7540.md) redeem claim functions and MUST follow [ERC-7540](./eip-7540.md) semantics, operating only on the claimable balance. #### maxRedeem, maxWithdraw In the synchronous-redeem variant, these MUST return `0` when `block.timestamp` is outside `exitWindow(epochId())` and the vault has not terminated. In the asynchronous-redeem variant, these MUST follow [ERC-7540](./eip-7540.md) semantics, reflecting the claimable balance. The exit window gates redemptions through [maxRequestRedeem](#maxrequestredeem), not through `maxRedeem`. #### requestRedeem This method is part of the asynchronous-redeem variant only. MUST revert when `block.timestamp` is outside `exitWindow(epochId())`, except after termination. MUST revert when the requested shares exceed [maxRequestRedeem](#maxrequestredeem) for `owner`. #### previewRedeem, previewWithdraw In the asynchronous-redeem variant, these MUST revert, as required by [ERC-7540](./eip-7540.md) for an asynchronous redemption Vault. ### Request fulfillment In the asynchronous variants a request is fulfilled at some point after it is made, and only then does it become claimable. This standard does not constrain when fulfillment happens or by what mechanism: the timing is at the implementation's discretion. A vault MAY fulfill an epoch's requests after that epoch's period has ended. Callers MUST rely on the [ERC-7540](./eip-7540.md) accessors to observe a request's state: `pendingDepositRequest` and `claimableDepositRequest` in the asynchronous-deposit variant, `pendingRedeemRequest` and `claimableRedeemRequest` in the asynchronous-redeem variant, together with the corresponding `max*` methods described in the [Inherited method behavior](#inherited-method-behavior) section. It is RECOMMENDED that the span between a request and its fulfillment be bounded and disclosed, so that a user can reason about how long their capital may be locked. See the [Fulfillment delay griefing](#fulfillment-delay-griefing) section. ### Events #### EpochAdded A new epoch has been created. All timestamp fields are absolute, matching the interval accessors in the [Methods](#methods) section. MUST be emitted when a new epoch is created. An epoch MAY be created before its `start`, and it becomes the current epoch when `block.timestamp` reaches `start`. ```yaml - name: EpochAdded type: event inputs: - name: epochId indexed: true type: uint256 - name: rate indexed: false type: uint256 - name: maxDeposits indexed: false type: uint256 - name: start indexed: false type: uint256 - name: entryWindowEnd indexed: false type: uint256 - name: accrualStart indexed: false type: uint256 - name: accrualEnd indexed: false type: uint256 - name: exitWindowStart indexed: false type: uint256 - name: end indexed: false type: uint256 ``` #### Terminated The vault has been permanently wound down. MUST be emitted exactly once, when the vault terminates. ```yaml - name: Terminated type: event inputs: [] ``` ### Limbo gap and epoch ordering Two created epochs `id` and `id + 1` MAY be contiguous, so that `epochPeriod(id + 1).start == epochPeriod(id).end`, or a limbo gap MAY separate them, so that `epochPeriod(id + 1).start > epochPeriod(id).end`, during which no epoch is current. In the asynchronous-redeem variant, an epoch's redemption requests MAY be fulfilled after the next epoch has started, which is what lets that epoch accrue with little or no dead time. Conversely, an implementation MAY defer the next epoch until those requests are fulfilled, producing a settlement gap between epochs. ### Termination A vault supports permanent termination, a one-way wind-down. Termination MUST occur outside an epoch. After termination no further epoch may start, no new deposit or deposit request may succeed, redemption is permanently open, and all outstanding asynchronous requests become claimable. Termination MUST NOT block a claim. A claimable balance MUST remain claimable after termination: the claim functions, which are `deposit` and `mint` in the asynchronous-deposit variant and `redeem` and `withdraw` in the asynchronous-redeem variant, MUST NOT revert on account of the terminated state, and their corresponding `max*` functions MUST NOT return `0` on account of it. Termination gates new capital, through `requestDeposit` and [maxRequestDeposit](#maxrequestdeposit). It does not gate claims of capital the vault has already taken in. Since all pending requests become claimable immediately, the implementation MUST ensure the vault has enough liquidity to cover all redemptions (e.g., do not allow termination if the vault's balance is insufficient). The mechanism that triggers termination is out of scope for this standard. ### Interfaces ```solidity interface IERC8416 { event EpochAdded( uint256 indexed epochId, uint256 rate, uint256 maxDeposits, uint256 start, uint256 entryWindowEnd, uint256 accrualStart, uint256 accrualEnd, uint256 exitWindowStart, uint256 end ); event Terminated(); function epochId() external view returns (uint256 id); function rate(uint256 id) external view returns (uint256 rate); function maxEpochDeposits(uint256 id) external view returns (uint256 maxAssets); function epochPeriod(uint256 id) external view returns (uint256 start, uint256 end); function entryWindow(uint256 id) external view returns (uint256 start, uint256 end); function accrualInterval(uint256 id) external view returns (uint256 start, uint256 end); function exitWindow(uint256 id) external view returns (uint256 start, uint256 end); function currentRate() external view returns (uint256 rate); function isTerminated() external view returns (bool terminated); } /// @dev Implemented together with `IERC7540Deposit` by asynchronous-deposit vaults. interface IERC8416AsyncDeposit is IERC8416 { function maxRequestDeposit(address owner) external view returns (uint256 maxAssets); } /// @dev Implemented together with `IERC7540Redeem` by asynchronous-redeem vaults. interface IERC8416AsyncRedeem is IERC8416 { function maxRequestRedeem(address owner) external view returns (uint256 maxShares); } ``` ## Rationale ### Independent Synchronicity of Deposits and Redemptions Modeling each leg as either an atomic [ERC-4626](./eip-4626.md) call or an [ERC-7540](./eip-7540.md) request, fulfill, and claim lifecycle lets the standard cover fully synchronous vaults, fully asynchronous vaults, and any mix, without defining a separate interface per combination. ### A Single Fungible Share Token Across Epochs Because the share remains a single fungible [ERC-20](./eip-20.md) across maturities, transfers, balances, and share-price reads need no special handling, and an integrator lists and prices one token indefinitely rather than relisting a new token each term. ### Non-inclusion of a Request Cancelation Flow This standard does not define a request cancelation flow. The asynchronous legs inherit the [ERC-7540](./eip-7540.md) request, fulfill, and claim lifecycle, and [ERC-7540](./eip-7540.md) itself leaves cancelation out. Not mandating it keeps the standard minimal and gives implementations the flexibility to decide whether, when, and how a pending request may be withdrawn. Implementations that want to offer this feature MAY adopt the [ERC-7887](./eip-7887.md) cancelation lifecycle or an equivalent mechanism. ### Fulfillment Timing Left to the Implementation [ERC-7540](./eip-7540.md) already exposes the pending and claimable state a caller needs without prescribing when the transition happens, and implementations legitimately differ: some fulfill on a fixed delay, some in operator-triggered batches, some at the moment assets move to or from the yield source. Leaving the timing open keeps the standard minimal and lets a vault pick the mechanism its strategy requires, while `pending*` and `claimable*` keep the lifecycle observable. ## Backwards Compatibility The vault is a conformant [ERC-20](./eip-20.md) token, so existing [ERC-20](./eip-20.md) tooling works for transfers in every variant without modification. A vault whose deposit and redemption legs are both synchronous is a conformant [ERC-4626](./eip-4626.md) Vault, so existing [ERC-4626](./eip-4626.md) tooling works for deposits and redemptions without modification, subject only to the windows gating when those calls succeed. On a leg implemented as an asynchronous variant, this standard inherits [ERC-7540](./eip-7540.md)'s deliberate departures from [ERC-4626](./eip-4626.md): the corresponding `preview*` functions revert, and `deposit` and `mint`, or `redeem` and `withdraw`, are claim functions rather than atomic entry and exit. [ERC-4626](./eip-4626.md) tooling that calls those `preview*` functions, or that expects an atomic call to move assets, must be [ERC-7540](./eip-7540.md)-aware. The asynchronous-deposit and asynchronous-redeem variants are conformant [ERC-7540](./eip-7540.md) deposit and redemption Vaults respectively, detectable via the [ERC-7540](./eip-7540.md) and [ERC-7575](./eip-7575.md) interface identifiers. Inherited event signatures are unchanged in every variant, so existing indexers decode them correctly. ## Security Considerations ### Solvency and the fixed-rate promise A fixed rate is a promise the vault makes about its share price. Interest accrues over the accrual interval, as described in the [Rate semantics](#rate-semantics) section, and it's strongly recommended that `totalAssets` reflect that accrual, so the share price rises deterministically toward maturity, whether or not the underlying strategy actually earned it. Implementers MUST consider the case where realized strategy yield falls short of the promised accrual at maturity. The failure mode when the vault is under-collateralized at maturity is determined entirely by how `totalAssets` behaves under a shortfall, together with `maxRedeem` and `maxWithdraw` in the synchronous variant, or `maxRequestRedeem` in the asynchronous variant. For example: - Socialized losses. If `totalAssets` reports the vault's true holdings rather than the promised value, every share redeems at the same reduced price, and the loss is shared across all holders. - First come, first served. If `totalAssets` keeps reporting the promised, fully accrued value while the vault actually holds less, redemptions pay out at the promised price until the assets run out, resulting in a bank run. ### `totalAssets` accrual and maturity front-running The interest reported by [`totalAssets`](#totalassets) SHOULD be recognized continuously across the accrual interval, so that the share price rises smoothly toward maturity. A discontinuous jump may be exploitable, because an attacker can front-run the price jump and capture value unfairly. Continuous accrual removes the discontinuity: there is no jump to front-run, and a deposit late in the accrual interval earns only the portion of interest that accrues for the duration it is actually held. Implementations that do not accrue continuously SHOULD NOT overlap the entry and exit windows with the accrual interval. This mitigates the front-running scenario, but note that the same discontinuity can be arbitraged on secondary markets even when direct deposits are gated. ### Share price at request and fulfillment in the asynchronous variants In the asynchronous variants there is a delay between a request and the claim, and the share price moves during that delay as interest accrues and epochs roll over. The price at which a request is fulfilled is therefore a security-relevant design choice, and this standard leaves it open. Implementations must consider and handle the side effects. For example: - If a deposit request is fulfilled at a price locked at request time, the depositor may earn interest over that delay even though their assets have not funded the yield source for that span. - If a redemption request is fulfilled at a price locked at claim time, and the requested assets have not been allocated to the yield source for some time, the total accrued amount reported by `totalAssets` may not represent the actual value. It is RECOMMENDED that a vault define a set of fulfillment functions that price shares at the moment of allocation to, or deallocation from, the yield source. Such functions are out of scope for this standard. ### Fulfillment delay griefing Because fulfillment timing is at the implementation's discretion, a redeemer's assets are locked from the moment the request is made until it is fulfilled and claimed, and this span MAY extend into subsequent epochs. A delay that is too long can strand a user's funds well past their intended exit. It is RECOMMENDED that implementations bound and disclose the maximum fulfillment delay, whether that is a cooldown, a batch cadence, or another mechanism. Entry windows, exit windows, and limbo gaps SHOULD be long enough to settle the strategy and short enough that funds are never locked for an unreasonable time. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).