Skip to content

Commit a28fc78

Browse files
authored
Add abstract ERC7540 and specific implementations (#228)
1 parent a12b30c commit a28fc78

29 files changed

Lines changed: 4125 additions & 96 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 28-04-2026
2+
3+
- `IERC7540`: Add interface for ERC-7540 asynchronous tokenized vaults, extending ERC-4626 with request-based deposit and redeem flows.
4+
- `IERC7575`: Add interface for ERC-7575 multi-asset vaults.
5+
- `ERC7540`: Add base implementation of ERC-7540 with operator management, configurable share custody, `totalAssets`/`totalSupply` adjustments to keep the share price accurate during the async lifecycle, and virtual hooks for strategy extensions.
6+
- `ERC7540AdminDeposit`, `ERC7540AdminRedeem`: Add admin-controlled fulfillment strategies where a privileged caller transitions requests from Pending to Claimable with an explicit exchange rate.
7+
- `ERC7540DelayDeposit`, `ERC7540DelayRedeem`: Add time-based fulfillment strategies that make requests claimable after a configurable delay, using `Checkpoints.Trace208` to accumulate maturing amounts.
8+
- `ERC7540SyncDeposit`, `ERC7540SyncRedeem`: Add synchronous strategies that preserve standard ERC-4626 behavior on one side of the vault.
9+
110
## 15-12-2025
211

312
- Remove `IERC7802`, `IERC7821` and `IERC7786`. These interfaces were migrated to `@openzeppelin/contracts>=5.5.0`.

contracts/interfaces/IERC7540.sol

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.8.4;
4+
5+
import {IERC7575} from "./IERC7575.sol";
6+
7+
/**
8+
* @dev Interface for operator management in https://eips.ethereum.org/EIPS/eip-7540[ERC-7540]
9+
* asynchronous vaults. Operators can manage deposit and redeem requests on behalf of a controller.
10+
*/
11+
interface IERC7540Operator {
12+
/// @dev Emitted when `controller` sets the `approved` status for an `operator`.
13+
event OperatorSet(address indexed controller, address indexed operator, bool approved);
14+
15+
/**
16+
* @dev Grants or revokes permissions for `operator` to manage requests on behalf of the caller.
17+
*
18+
* - MUST set the operator status to the `approved` value.
19+
* - MUST emit the {OperatorSet} event when the operator status is set.
20+
* - MUST return true.
21+
*/
22+
function setOperator(address operator, bool approved) external returns (bool);
23+
24+
/// @dev Returns `true` if the `operator` is approved as an operator for a `controller`.
25+
function isOperator(address controller, address operator) external view returns (bool status);
26+
}
27+
28+
/**
29+
* @dev Interface for asynchronous deposit requests in https://eips.ethereum.org/EIPS/eip-7540[ERC-7540]
30+
* vaults. Enables users to request deposits that transition through Pending and Claimable states
31+
* before being claimed via the standard ERC-4626 deposit/mint functions.
32+
*/
33+
interface IERC7540Deposit {
34+
/**
35+
* @dev Emitted when `owner` has locked `assets` in the Vault to request a deposit.
36+
* `controller` controls this request. `sender` is the caller of `requestDeposit`.
37+
*/
38+
event DepositRequest(
39+
address indexed controller,
40+
address indexed owner,
41+
uint256 indexed requestId,
42+
address sender,
43+
uint256 assets
44+
);
45+
46+
/**
47+
* @dev Transfers `assets` from `owner` into the Vault and submits a request for asynchronous deposit.
48+
*
49+
* - MUST support ERC-20 approve / transferFrom on `asset` as a deposit request flow.
50+
* - `owner` MUST equal `msg.sender` unless the `owner` has approved the `msg.sender` as an operator.
51+
* - MUST revert if all of `assets` cannot be requested for deposit.
52+
* - MUST emit the {DepositRequest} event.
53+
*
54+
* NOTE: Most implementations will require `owner` to have approved the Vault to spend at least `assets` of
55+
* the underlying asset token (e.g. via `asset.approve(vault, assets)`) before calling this function.
56+
*/
57+
function requestDeposit(uint256 assets, address controller, address owner) external returns (uint256 requestId);
58+
59+
/**
60+
* @dev Returns the amount of requested assets in Pending state.
61+
*
62+
* - MUST NOT include any assets in Claimable state for deposit or mint.
63+
* - MUST NOT show any variations depending on the caller.
64+
* - MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
65+
*/
66+
function pendingDepositRequest(uint256 requestId, address controller) external view returns (uint256 pendingAssets);
67+
68+
/**
69+
* @dev Returns the amount of requested assets in Claimable state for the controller to deposit or mint.
70+
*
71+
* - MUST NOT include any assets in Pending state.
72+
* - MUST NOT show any variations depending on the caller.
73+
* - MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
74+
*/
75+
function claimableDepositRequest(
76+
uint256 requestId,
77+
address controller
78+
) external view returns (uint256 claimableAssets);
79+
80+
/**
81+
* @dev Claims a pending deposit request by minting shares to receiver.
82+
* Decreases `claimableDepositRequest` for the controller and transfers shares to receiver.
83+
*
84+
* - MUST emit the Deposit event.
85+
* - MUST revert if controller does not have sufficient claimable assets.
86+
* - controller MUST equal msg.sender unless the controller has approved the msg.sender as an operator.
87+
*/
88+
function deposit(uint256 assets, address receiver, address controller) external returns (uint256 shares);
89+
90+
/**
91+
* @dev Claims a pending deposit request by minting exactly shares to receiver.
92+
* Decreases `claimableDepositRequest` for the controller and transfers shares to receiver.
93+
*
94+
* - MUST emit the Deposit event.
95+
* - MUST revert if controller does not have sufficient claimable assets.
96+
* - controller MUST equal msg.sender unless the controller has approved the msg.sender as an operator.
97+
*/
98+
function mint(uint256 shares, address receiver, address controller) external returns (uint256 assets);
99+
}
100+
101+
/**
102+
* @dev Interface for asynchronous redeem requests in https://eips.ethereum.org/EIPS/eip-7540[ERC-7540]
103+
* vaults. Enables users to request redemptions that transition through Pending and Claimable states
104+
* before being claimed via the standard ERC-4626 redeem/withdraw functions.
105+
*/
106+
interface IERC7540Redeem {
107+
/**
108+
* @dev Emitted when `sender` has locked `shares`, owned by `owner`, in the Vault to request a redemption.
109+
* `controller` controls this request.
110+
*/
111+
event RedeemRequest(
112+
address indexed controller,
113+
address indexed owner,
114+
uint256 indexed requestId,
115+
address sender,
116+
uint256 shares
117+
);
118+
119+
/**
120+
* @dev Assumes control of shares from owner into the Vault and submits a request for asynchronous redeem.
121+
*
122+
* Authorization for a `msg.sender` not equal to `owner` may come either from ERC-20 approval over
123+
* the shares of `owner` or from an operator approval via {IERC7540Operator-setOperator}. Operators
124+
* are not subject to allowance restrictions, while non-infinite ERC-20 approvals are consumed.
125+
*
126+
* - MUST revert if all of shares cannot be requested for redeem.
127+
* - MUST emit the {RedeemRequest} event.
128+
*
129+
* NOTE: Most implementations will require `owner` to have approved the Vault to spend at least `shares` of
130+
* the Vault's share token (e.g. via `share.approve(vault, shares)`) before calling this function.
131+
*/
132+
function requestRedeem(uint256 shares, address controller, address owner) external returns (uint256 requestId);
133+
134+
/**
135+
* @dev Returns the amount of requested shares in Pending state.
136+
*
137+
* - MUST NOT include any shares in Claimable state for redeem or withdraw.
138+
* - MUST NOT show any variations depending on the caller.
139+
* - MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
140+
*/
141+
function pendingRedeemRequest(uint256 requestId, address controller) external view returns (uint256 pendingShares);
142+
143+
/**
144+
* @dev Returns the amount of requested shares in Claimable state for the controller to redeem or withdraw.
145+
*
146+
* - MUST NOT include any shares in Pending state for redeem or withdraw.
147+
* - MUST NOT show any variations depending on the caller.
148+
* - MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
149+
*/
150+
function claimableRedeemRequest(
151+
uint256 requestId,
152+
address controller
153+
) external view returns (uint256 claimableShares);
154+
}
155+
156+
/**
157+
* @dev Interface of the fully asynchronous Vault interface of ERC7540, as defined in
158+
* https://eips.ethereum.org/EIPS/eip-7540
159+
*/
160+
interface IERC7540 is IERC7540Operator, IERC7540Deposit, IERC7540Redeem, IERC7575 {}

contracts/interfaces/IERC7575.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.8.4;
4+
5+
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
6+
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
7+
8+
/**
9+
* @dev Multi-Asset ERC-4626 Vaults, as defined in https://eips.ethereum.org/EIPS/eip-7575
10+
*/
11+
interface IERC7575 is IERC165, IERC4626 {
12+
/// @dev The address of the underlying share received on deposit into the Vault.
13+
function share() external view returns (address shareTokenAddress);
14+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol";
6+
import {ERC7540AdminDeposit} from "../../token/ERC20/extensions/ERC7540AdminDeposit.sol";
7+
import {ERC7540AdminRedeem} from "../../token/ERC20/extensions/ERC7540AdminRedeem.sol";
8+
9+
abstract contract ERC7540AdminMock is ERC7540AdminDeposit, ERC7540AdminRedeem {
10+
address private immutable _tmpShareHolder;
11+
12+
constructor(address tmpShareHolder) {
13+
_tmpShareHolder = tmpShareHolder;
14+
}
15+
16+
function _requestDeposit(
17+
uint256 assets,
18+
address controller,
19+
address owner,
20+
uint256 requestId
21+
) internal virtual override(ERC7540, ERC7540AdminDeposit) returns (uint256) {
22+
return super._requestDeposit(assets, controller, owner, requestId);
23+
}
24+
25+
function _requestRedeem(
26+
uint256 shares,
27+
address controller,
28+
address owner,
29+
uint256 requestId
30+
) internal virtual override(ERC7540, ERC7540AdminRedeem) returns (uint256) {
31+
return super._requestRedeem(shares, controller, owner, requestId);
32+
}
33+
34+
function _depositShareOrigin() internal view virtual override returns (address) {
35+
return _tmpShareHolder;
36+
}
37+
38+
function _redeemShareDestination() internal view virtual override returns (address) {
39+
return _tmpShareHolder;
40+
}
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol";
6+
import {ERC7540DelayDeposit} from "../../token/ERC20/extensions/ERC7540DelayDeposit.sol";
7+
import {ERC7540DelayRedeem} from "../../token/ERC20/extensions/ERC7540DelayRedeem.sol";
8+
9+
abstract contract ERC7540DelayMock is ERC7540DelayDeposit, ERC7540DelayRedeem {
10+
function clock() public view virtual override(ERC7540DelayDeposit, ERC7540DelayRedeem) returns (uint48) {
11+
return super.clock();
12+
}
13+
14+
function CLOCK_MODE()
15+
public
16+
view
17+
virtual
18+
override(ERC7540DelayDeposit, ERC7540DelayRedeem)
19+
returns (string memory)
20+
{
21+
return super.CLOCK_MODE();
22+
}
23+
24+
function _requestDeposit(
25+
uint256 assets,
26+
address controller,
27+
address owner,
28+
uint256 requestId
29+
) internal virtual override(ERC7540, ERC7540DelayDeposit) returns (uint256) {
30+
return super._requestDeposit(assets, controller, owner, requestId);
31+
}
32+
33+
function _requestRedeem(
34+
uint256 shares,
35+
address controller,
36+
address owner,
37+
uint256 requestId
38+
) internal virtual override(ERC7540, ERC7540DelayRedeem) returns (uint256) {
39+
return super._requestRedeem(shares, controller, owner, requestId);
40+
}
41+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol";
6+
import {ERC7540SyncDeposit} from "../../token/ERC20/extensions/ERC7540SyncDeposit.sol";
7+
import {ERC7540AdminRedeem} from "../../token/ERC20/extensions/ERC7540AdminRedeem.sol";
8+
9+
abstract contract ERC7540SyncDepositMock is ERC7540SyncDeposit, ERC7540AdminRedeem {
10+
function _requestRedeem(
11+
uint256 shares,
12+
address controller,
13+
address owner,
14+
uint256 requestId
15+
) internal virtual override(ERC7540, ERC7540AdminRedeem) returns (uint256) {
16+
return super._requestRedeem(shares, controller, owner, requestId);
17+
}
18+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol";
6+
import {ERC7540SyncDeposit} from "../../token/ERC20/extensions/ERC7540SyncDeposit.sol";
7+
import {ERC7540SyncRedeem} from "../../token/ERC20/extensions/ERC7540SyncRedeem.sol";
8+
9+
abstract contract ERC7540SyncMock is ERC7540SyncDeposit, ERC7540SyncRedeem {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol";
6+
import {ERC7540SyncRedeem} from "../../token/ERC20/extensions/ERC7540SyncRedeem.sol";
7+
import {ERC7540AdminDeposit} from "../../token/ERC20/extensions/ERC7540AdminDeposit.sol";
8+
9+
abstract contract ERC7540SyncRedeemMock is ERC7540SyncRedeem, ERC7540AdminDeposit {
10+
function _requestDeposit(
11+
uint256 assets,
12+
address controller,
13+
address owner,
14+
uint256 requestId
15+
) internal virtual override(ERC7540, ERC7540AdminDeposit) returns (uint256) {
16+
return super._requestDeposit(assets, controller, owner, requestId);
17+
}
18+
}

0 commit comments

Comments
 (0)