Skip to content

Add ERC7540ControlledRedeem / ERC7540ControlledDeposit: concrete controlled-fulfillment extensions for ERC-7540 #6446

@ernestognw

Description

@ernestognw

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions