EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8416: Epoch-Based Fixed-Rate Vault

An ERC-4626 vault that progresses through a sequence of fixed-rate epochs

Authors
Created2026-06-26
Discussion Linkhttps://ethereum-magicians.org/t/erc-8416-epoch-based-fixed-rate-vault/29669
Requires

Markdown

https://raw.githubusercontent.com/marcelomorgado/E...

EIP-GPT summary

Contents
AbstractMotivationSpecificationDefinitionsConformanceERC-165 supportRate semanticsMethodsepochIdratemaxEpochDepositsepochPeriodentryWindowaccrualIntervalexitWindowmaxRequestDepositmaxRequestRedeemcurrentRateisTerminatedInherited method behaviordeposit, mintmaxDeposit, maxMintrequestDepositpreviewDeposit, previewMinttotalAssetsredeem, withdrawmaxRedeem, maxWithdrawrequestRedeempreviewRedeem, previewWithdrawRequest fulfillmentEventsEpochAddedTerminatedLimbo gap and epoch orderingTerminationInterfacesRationaleIndependent Synchronicity of Deposits and RedemptionsA Single Fungible Share Token Across EpochsNon-inclusion of a Request Cancelation FlowFulfillment Timing Left to the ImplementationBackwards CompatibilitySecurity ConsiderationsSolvency and the fixed-rate promisetotalAssets accrual and maturity front-runningShare price at request and fulfillment in the asynchronous variantsFulfillment delay griefingCopyright

Abstract

The following standard extends ERC-4626 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) or asynchronous (ERC-7540 request, fulfill, and claim). Each chosen variant is discoverable via ERC-165. Shares stay fungible across epochs because the vault rolls over the share-to-asset price.

Motivation

Because ERC-4626 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 for share-price mechanics and for gated entry and exit, and ERC-7540 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 and ERC-7540 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 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 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, ERC-20, ERC-165, and the base interface IERC8416 defined in the 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 identifier is absent.

Deposit variants:

  • Synchronous-deposit. Deposits are atomic ERC-4626 deposit and mint calls. The vault MUST NOT implement IERC7540Deposit.
  • Asynchronous-deposit. Deposits follow the ERC-7540 request, fulfillment, and claim lifecycle. The vault MUST implement both IERC8416AsyncDeposit and IERC7540Deposit.

Redemption variants:

  • Synchronous-redeem. Redemptions are atomic ERC-4626 redeem and withdraw calls. The vault MUST NOT implement IERC7540Redeem.
  • Asynchronous-redeem. Redemptions follow the ERC-7540 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 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 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 asynchronous redemption identifier) is passed through the interfaceID argument. Vaults implementing the synchronous-redeem variant MUST return false for that argument.

InterfaceERC-165 identifiertrue when
IERC8416 (base)type(IERC8416).interfaceIdAlways, for every conformant vault
IERC7540Deposit0xce3bbe50Asynchronous-deposit variant
IERC7540Redeem0x620ee8e4Asynchronous-redeem variant
IERC7575the ERC-7575 identifierEither asynchronous variant, as required by ERC-7540

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 identifier MAY therefore rely on the corresponding maxRequestDeposit or 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.

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

MUST NOT revert.

MUST be immutable once the epoch has been created.

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

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

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

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

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

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

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

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

- name: currentRate
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: rate
      type: uint256

isTerminated

Whether the vault has been permanently wound down. See the Termination section.

MUST NOT revert.

Once true, MUST remain true.

- name: isTerminated
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: terminated
      type: bool

Inherited method behavior

This standard constrains ERC-4626 methods, and, in the asynchronous variants, ERC-7540 methods, as follows. All other ERC-4626 and ERC-7540 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 deposit claim functions and MUST follow ERC-7540 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 section.

In the asynchronous-deposit variant, these MUST follow ERC-7540 semantics, reflecting the claimable balance. The deposit capacity gates new capital through 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 for owner.

previewDeposit, previewMint

In the asynchronous-deposit variant, these MUST revert, as required by ERC-7540 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 redeem claim functions and MUST follow ERC-7540 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 semantics, reflecting the claimable balance. The exit window gates redemptions through 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 for owner.

previewRedeem, previewWithdraw

In the asynchronous-redeem variant, these MUST revert, as required by ERC-7540 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 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 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 section.

Events

EpochAdded

A new epoch has been created. All timestamp fields are absolute, matching the interval accessors in the 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.

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

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

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 call or an ERC-7540 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 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 request, fulfill, and claim lifecycle, and ERC-7540 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 cancelation lifecycle or an equivalent mechanism.

Fulfillment Timing Left to the Implementation

ERC-7540 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 token, so existing ERC-20 tooling works for transfers in every variant without modification.

A vault whose deposit and redemption legs are both synchronous is a conformant ERC-4626 Vault, so existing ERC-4626 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's deliberate departures from ERC-4626: the corresponding preview* functions revert, and deposit and mint, or redeem and withdraw, are claim functions rather than atomic entry and exit. ERC-4626 tooling that calls those preview* functions, or that expects an atomic call to move assets, must be ERC-7540-aware. The asynchronous-deposit and asynchronous-redeem variants are conformant ERC-7540 deposit and redemption Vaults respectively, detectable via the ERC-7540 and ERC-7575 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 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 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 and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth