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
depositandmintcalls. The vault MUST NOT implementIERC7540Deposit. - Asynchronous-deposit. Deposits follow the ERC-7540 request, fulfillment, and claim lifecycle. The vault MUST implement both
IERC8416AsyncDepositandIERC7540Deposit.
Redemption variants:
- Synchronous-redeem. Redemptions are atomic ERC-4626
redeemandwithdrawcalls. The vault MUST NOT implementIERC7540Redeem. - Asynchronous-redeem. Redemptions follow the ERC-7540 request, fulfillment, and claim lifecycle. The vault MUST implement both
IERC8416AsyncRedeemandIERC7540Redeem.
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.
| 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 identifier | Either 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: uint256rate
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: uint256maxEpochDeposits
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: uint256epochPeriod
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: uint256entryWindow
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: uint256accrualInterval
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: uint256exitWindow
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: uint256maxRequestDeposit
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: uint256maxRequestRedeem
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: uint256currentRate
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: uint256isTerminated
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: boolInherited 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: uint256Terminated
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
totalAssetsreports 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
totalAssetskeeps 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
totalAssetsmay 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.
