Part of #4761 / #6399.
Summary
Add two abstract extensions on top of the ERC7540Redeem and ERC7540Deposit abstract base contracts that implement controlled fulfillment: a privileged caller explicitly triggers fulfillment for individual controllers or in batches.
ERC7540ControlledRedeem extends ERC7540Redeem (abstract)
ERC7540ControlledDeposit extends ERC7540Deposit (abstract)
Motivation
Most production ERC-7540 vaults require a privileged actor (owner, operator, off-chain settlement bot) to decide when and how much to settle. The access control mechanism varies widely across deployments: Centrifuge uses an off-chain epoch executor, Nest (Plume) uses onlyOwner, USDai uses a FIFO queue behind an admin role. Rather than encoding a single access control strategy, these contracts follow the same pattern as UUPSUpgradeable._authorizeUpgrade: they expose an internal virtual hook that implementations decorate with their chosen modifier.
Proposed interface
abstract contract ERC7540ControlledRedeem is ERC7540Redeem {
event RedeemFulfilled(address indexed controller, uint256 shares, uint256 assets);
function fulfillRedeem(address controller, uint256 shares) external virtual returns (uint256 assets);
function fulfillMultipleRedeems(address[] calldata controllers, uint256[] calldata shares) external virtual returns (uint256[] memory assets);
/**
* @dev Authorization hook called before every fulfillment. Override with the
* access control check of your choice:
*
* ```solidity
* function _authorizeFulfillRedeem() internal override onlyOwner {}
* function _authorizeFulfillRedeem() internal override onlyRole(FULFILLER_ROLE) {}
* ```
*/
function _authorizeFulfillRedeem() internal virtual;
}
ERC7540ControlledDeposit is symmetric with _authorizeFulfillDeposit().
Behavior
fulfillRedeem(controller, shares) calls _authorizeFulfillRedeem() before any state change, then calls _fulfillRedeem(shares, controller) and emits RedeemFulfilled.
fulfillMultipleRedeems iterates and calls the same logic per entry — no application-specific ordering.
- Partial fulfillment is supported:
shares can be less than pendingRedeemRequest (handled by the base _fulfillRedeem).
- For redeems, the fulfiller must ensure the vault holds sufficient assets before calling (asset sourcing is application-specific).
- For deposits, the fulfiller should only call after the deposited assets are reflected in
totalAssets() to avoid diluting existing holders.
- These contracts are intentionally abstract — they cannot be deployed without overriding
_authorizeFulfillRedeem / _authorizeFulfillDeposit.
Relationship with the Contracts Wizard
Because the only deployment-specific addition is the _authorize* override, these are strong candidates for Contracts Wizard integration. The Wizard would let developers pick their access control flavor (Ownable, AccessControl with a custom role, custom address) and emit a ready-to-deploy vault:
// Ownable
contract MyVault is ERC7540ControlledRedeem, Ownable {
constructor(IERC20 asset_, address owner) ERC7540Operator(asset_) Ownable(owner) {}
function _authorizeFulfillRedeem() internal override onlyOwner {}
}
// AccessControl with a fulfiller role
contract MyVault is ERC7540ControlledRedeem, AccessControl {
bytes32 public constant FULFILLER_ROLE = keccak256("FULFILLER_ROLE");
constructor(IERC20 asset_, address admin) ERC7540Operator(asset_) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(FULFILLER_ROLE, admin);
}
function _authorizeFulfillRedeem() internal override onlyRole(FULFILLER_ROLE) {}
}
Open questions
- Are the library contracts needed at all? Because the concrete surface is just the
_authorize* override, the Wizard could generate complete vaults directly from ERC7540Redeem / ERC7540Deposit without an intermediate abstract parent. Should these abstract contracts be shipped in the library, or treated as Wizard-only templates?
- Batch function inclusion. Should
fulfillMultipleRedeems / fulfillMultipleDeposits be part of the library contract or left as a documented pattern?
Part of #4761 / #6399.
Summary
Add two abstract extensions on top of the
ERC7540RedeemandERC7540Depositabstract base contracts that implement controlled fulfillment: a privileged caller explicitly triggers fulfillment for individual controllers or in batches.ERC7540ControlledRedeem extends ERC7540Redeem(abstract)ERC7540ControlledDeposit extends ERC7540Deposit(abstract)Motivation
Most production ERC-7540 vaults require a privileged actor (owner, operator, off-chain settlement bot) to decide when and how much to settle. The access control mechanism varies widely across deployments: Centrifuge uses an off-chain epoch executor, Nest (Plume) uses
onlyOwner, USDai uses a FIFO queue behind an admin role. Rather than encoding a single access control strategy, these contracts follow the same pattern asUUPSUpgradeable._authorizeUpgrade: they expose an internal virtual hook that implementations decorate with their chosen modifier.Proposed interface
ERC7540ControlledDepositis symmetric with_authorizeFulfillDeposit().Behavior
fulfillRedeem(controller, shares)calls_authorizeFulfillRedeem()before any state change, then calls_fulfillRedeem(shares, controller)and emitsRedeemFulfilled.fulfillMultipleRedeemsiterates and calls the same logic per entry — no application-specific ordering.sharescan be less thanpendingRedeemRequest(handled by the base_fulfillRedeem).totalAssets()to avoid diluting existing holders._authorizeFulfillRedeem/_authorizeFulfillDeposit.Relationship with the Contracts Wizard
Because the only deployment-specific addition is the
_authorize*override, these are strong candidates for Contracts Wizard integration. The Wizard would let developers pick their access control flavor (Ownable, AccessControl with a custom role, custom address) and emit a ready-to-deploy vault:Open questions
_authorize*override, the Wizard could generate complete vaults directly fromERC7540Redeem/ERC7540Depositwithout an intermediate abstract parent. Should these abstract contracts be shipped in the library, or treated as Wizard-only templates?fulfillMultipleRedeems/fulfillMultipleDepositsbe part of the library contract or left as a documented pattern?