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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions