Part of #4761 / #6399.
Summary
Add two concrete, non-abstract extensions on top of the ERC7540Redeem and ERC7540Deposit abstract base contracts that implement delayed fulfillment: fulfillment becomes permissionless after a configurable waiting period elapses since the request was submitted.
ERC7540DelayedRedeem extends ERC7540Redeem, Ownable
ERC7540DelayedDeposit extends ERC7540Deposit, Ownable
Motivation
The most common reason for using an async vault is that redemptions (and sometimes deposits) cannot be settled immediately — the vault must first unwind a position (unstaking, RWA liquidation, cross-chain bridge). In the simplest cases the unwinding takes a known, fixed duration. After that duration any actor (the controller, a keeper, a bot) can trigger settlement without requiring a privileged transaction.
Real-world examples: BeefySonic (protocol-dictated SFC unbonding period), MagmaV2 (owner-configurable delay), Tangle.
Proposed interface
contract ERC7540DelayedRedeem is ERC7540Redeem, Ownable {
error ERC7540DelayNotElapsed(uint256 requestedAt, uint256 delay, uint256 currentTime);
error ERC7540DelayTooLong(uint256 delay, uint256 maxDelay);
event DelayUpdated(uint256 oldDelay, uint256 newDelay);
event RedeemFulfilled(address indexed controller, uint256 shares, uint256 assets);
uint256 public delay;
uint256 public constant MAX_DELAY = 365 days;
constructor(IERC20 asset_, address initialOwner, uint256 initialDelay);
function setDelay(uint256 newDelay) external onlyOwner;
function requestRedeem(uint256 shares, address controller, address owner) public override returns (uint256);
function fulfillRedeem(address controller) external returns (uint256 assets);
}
ERC7540DelayedDeposit is symmetric: same storage, same constructor shape, requestDeposit override, and fulfillDeposit(address controller).
Behavior
- On
requestRedeem / requestDeposit, block.timestamp is recorded for the controller.
fulfillRedeem(controller) / fulfillDeposit(controller) is permissionless — reverts with ERC7540DelayNotElapsed if block.timestamp < _requestedAt[controller] + delay. On success, calls _fulfillRedeem / _fulfillDeposit with the full pending balance at the live exchange rate.
- The
delay is owner-configurable but capped at MAX_DELAY to prevent locking funds.
- Pending shares are held in the vault during the wait (default
_lockSharesIn → _completeSharesIn flow), so they continue to accrue yield until burned at fulfillment.
- Pending deposit assets are excluded from
totalAssets() during the wait (handled by ERC7540Deposit), preventing dilution of existing holders.
Open questions
MAX_DELAY value. 365 days covers liquid staking unbonding periods; 28 days may be safer for general-purpose use.
- Timestamp accumulation policy. When a controller adds to an existing pending request, should the timestamp be kept at the earliest (first-in wins, recommended) or reset to the latest?
Composability
A vault that delays only redeems uses ERC7540DelayedRedeem alone. A fully delayed vault inherits from both extensions plus ERC7540:
contract MyFullyDelayedVault is ERC7540, ERC7540DelayedRedeem, ERC7540DelayedDeposit, Ownable { ... }
Part of #4761 / #6399.
Summary
Add two concrete, non-abstract extensions on top of the
ERC7540RedeemandERC7540Depositabstract base contracts that implement delayed fulfillment: fulfillment becomes permissionless after a configurable waiting period elapses since the request was submitted.ERC7540DelayedRedeem extends ERC7540Redeem, OwnableERC7540DelayedDeposit extends ERC7540Deposit, OwnableMotivation
The most common reason for using an async vault is that redemptions (and sometimes deposits) cannot be settled immediately — the vault must first unwind a position (unstaking, RWA liquidation, cross-chain bridge). In the simplest cases the unwinding takes a known, fixed duration. After that duration any actor (the controller, a keeper, a bot) can trigger settlement without requiring a privileged transaction.
Real-world examples: BeefySonic (protocol-dictated SFC unbonding period), MagmaV2 (owner-configurable delay), Tangle.
Proposed interface
ERC7540DelayedDepositis symmetric: same storage, same constructor shape,requestDepositoverride, andfulfillDeposit(address controller).Behavior
requestRedeem/requestDeposit,block.timestampis recorded for the controller.fulfillRedeem(controller)/fulfillDeposit(controller)is permissionless — reverts withERC7540DelayNotElapsedifblock.timestamp < _requestedAt[controller] + delay. On success, calls_fulfillRedeem/_fulfillDepositwith the full pending balance at the live exchange rate.delayis owner-configurable but capped atMAX_DELAYto prevent locking funds._lockSharesIn→_completeSharesInflow), so they continue to accrue yield until burned at fulfillment.totalAssets()during the wait (handled byERC7540Deposit), preventing dilution of existing holders.Open questions
MAX_DELAYvalue. 365 days covers liquid staking unbonding periods; 28 days may be safer for general-purpose use.Composability
A vault that delays only redeems uses
ERC7540DelayedRedeemalone. A fully delayed vault inherits from both extensions plusERC7540: