ERC-8226 - Regulated Agent Mandate

Created 2026-04-12
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This standard defines a compliance delegation layer for AI agents operating on tokenized regulated assets. It specifies how a verified principal can delegate scoped, time-bounded, and financially capped authority to an on-chain agent, and how a regulated token verifies the mandate before an agent-initiated action.

Regulated Agent Mandate Standard, or RAMS, is agnostic to the agent identity system, the token standard, and the token compliance framework. It works with any agent identity system (such as ERC-8004), any token standard (ERC-20, ERC-721, ERC-1155), and any regulated token standard (such as ERC-7943 or ERC-3643).

Motivation

The market for tokenized real-world assets is entering a phase of institutional adoption. Platforms operating under regulatory frameworks are starting to support programmable, agent-driven portfolio management on regulated instruments. AI agents that can autonomously execute securities transactions are no longer theoretical; they are being built now, without a standard that makes their operation legally defensible.

An agent purchasing a tokenized fund unit on behalf of an investor must satisfy three conditions that no existing standard addresses jointly:

  1. The principal on whose behalf the agent acts must be a verified, Know Your Customer (KYC)-cleared legal identity, not merely an Ethereum address.
  2. The mandate granted to the agent must be legally traceable, time-bounded, and financially capped, analogous to a power of attorney in traditional finance.
  3. The asset contract must validate the mandate atomically at execution time, without relying on off-chain coordination.

Regulated token standards such as ERC-7943 and ERC-3643 govern who may hold or transact a token, but neither defines an agent delegation model. Agent identity standards such as ERC-8004 provide agent discovery and trust signals but no mandate framework, and general-purpose agent authorization standards do not address regulated assets. RAMS defines the delegation interface, the compliance provider model, and the integration pattern with regulated token contracts.

The compliance responsibilities across the three layers are as follows:

Layer Responsibility Standard
Token compliance Investor eligibility on this specific asset Token compliance framework (e.g., ERC-7943, ERC-3643)
Mandate compliance Agent authority from this principal for this scope This ERC
Agent identity Agent exists and is registered Agent registry (e.g., ERC-8004)

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHOULD", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. All implementations MUST implement ERC-165.

Revert conditions in this standard are normative; the error selectors used to signal them are implementation-defined.

RAMS defines two interfaces. Implementations SHOULD deploy them as separate contracts so that an independent compliance operator and the registry operator can be governed separately; a single operator MAY combine them.

Interface Role Deployed by
IComplianceProvider Verifies principal eligibility (identity + compliance) Compliance operator or platform
IAgentMandate Mandate lifecycle, execution recording, freeze, and views RAMS registry operator

RAMS-aware regulated token contracts consult the RAMS registry on each function they let an agent perform for a holder.

IComplianceProvider

IComplianceProvider is implemented by a third-party compliance operator or platform (for example, a KYC provider or an on-chain identity registry adapter), deployed independently of the RAMS registry. Its address is supplied by the principal at mandate grant time via the complianceProvider field of IAgentMandate.grantMandate. A single IComplianceProvider instance MAY serve multiple mandates across multiple principals.

The compliance provider manages principal eligibility: granting, revoking, and checking whether a principal is eligible.

When used, checkPrincipal MUST return structured data sufficient for regulatory audit: a binary oracle is non-conformant, since reason codes and expiry timestamps are required for any credible compliance trail. It MUST verify that identityRef resolves to a valid, unrevoked attestation and return eligible == false if it does not.

The provider is mandatory: grantMandate MUST revert if complianceProvider is address(0).

grantPrincipal MUST emit PrincipalGranted and revokePrincipal MUST emit PrincipalRevoked, so eligibility changes are reconstructible from logs.

A principal's compliance window is the period during which checkPrincipal reports that principal eligible, bounded by expiresAt, where expiresAt == 0 denotes an unbounded window. The registry reads expiresAt only in grantMandate and extendMandate, where it bounds validUntil under it, and does not re-read it while a mandate is live. A provider therefore MUST NOT shrink the compliance window of a principal that remains eligible: it MUST NOT move expiresAt earlier, and MUST NOT move it from 0 to any timestamp, since an unbounded window is the widest window rather than the narrowest. Widening is unconstrained. Because the rule is stated over the window and not over the numeric value of expiresAt, a provider MUST NOT infer it from ordering of the timestamps alone. A provider that needs to narrow a principal's authority MUST call revokePrincipal and issue a new grant, so the change is signalled through PrincipalRevoked rather than applied silently to mandates already outstanding.

ReasonCode values MUST be appended without renumbering the existing ones. OTHER is reserved for conditions not listed and MUST NOT stand in for a listed code. A provider that cannot evaluate a check MUST revert rather than return eligible == false, so a failed evaluation is never reported as ineligibility.

interface IComplianceProvider is IERC165 {
    enum ReasonCode {
        COMPLIANT,             // 0
        KYC_EXPIRED,           // 1
        AML_FLAG,              // 2
        NOT_ACCREDITED,        // 3
        NOT_QUALIFIED,         // 4
        JURISDICTION_BLOCKED,  // 5
        IDENTITY_NOT_FOUND,    // 6
        ATTESTATION_REVOKED,   // 7
        OTHER                  // 8
    }

    /// @notice Emitted when a principal is granted eligibility.
    event PrincipalGranted(
        address indexed principal,
        bytes32 indexed identityRef
    );

    /// @notice Emitted when a previously eligible principal is revoked.
    event PrincipalRevoked(
        address indexed principal,
        bytes32 indexed identityRef,
        ReasonCode reason
    );

    /// @notice Grants eligibility to a principal.
    /// @param principal The on-chain address of the principal.
    /// @param identityRef An off-chain identity reference (e.g., keccak256 of a Decentralized Identifier (DID) or attestation ID).
    /// @param expiresAt Unix timestamp after which eligibility MUST be re-checked. 0 means no expiry.
    function grantPrincipal(address principal, bytes32 identityRef, uint48 expiresAt) external;

    /// @notice Revokes a principal's eligibility.
    /// @param principal The on-chain address of the principal.
    /// @param reason The reason for revocation.
    function revokePrincipal(address principal, ReasonCode reason) external;

    /// @notice Returns eligibility of a principal.
    /// @param principal The on-chain address of the principal.
    /// @param identityRef An off-chain identity reference (e.g., keccak256 of a Decentralized Identifier (DID) or attestation ID).
    /// @return eligible True if the principal is compliant.
    /// @return reason Reason code. MUST be COMPLIANT when eligible is true.
    /// @return expiresAt Unix timestamp after which this result MUST be re-checked. 0 means no expiry when eligible is true; when eligible is false the value carries no meaning, so consumers MUST read eligible and reason first.
    function checkPrincipal(address principal, bytes32 identityRef)
        external view returns (bool eligible, ReasonCode reason, uint48 expiresAt);
}

An IComplianceProvider implementation MAY delegate identity verification to on-chain identity standards, Ethereum Attestation Service (EAS) attestations, or any other identity backend. The interface is agnostic to the source.

IAgentMandate

IAgentMandate is implemented by the RAMS registry, a single contract deployed by a registry operator (e.g., a platform or a regulated entity acting as operator). Principals interact with this contract to grant, extend, and revoke mandates. Any contract in the agent's execution path calls canExecute to verify the mandate and recordExecution to record use.

Mandate storage is keyed by (agent, principal). Each (agent, principal) pair has at most one active mandate at any given time.

A mandate authorizes the agent to act on a set of actions on a specific asset. Actions are identified by bytes32 labels.

A value of type(uint256).max in maxTransactionValue or maxCumulativeValue signals "no limit."

Caps are denominated in the asset's transfer quantity: the token amount for ERC-20 and ERC-1155, and a token count (1 per transfer) for ERC-721. RAMS caps the quantity transferred, not specific token identifiers: a mandate is scoped to the asset address and covers every identifier that contract holds.

Freezing MUST be restricted to authorized enforcer roles defined by the implementation's access control, and the enforcer address MUST be recorded in the AgentFrozen and PrincipalFrozen events. A freeze halts evaluation but MUST NOT revoke: revocation stays with the principal, so an enforcer can stop a mandate but cannot destroy a delegation the principal wants to keep, and an enforcer MUST be able to lift a freeze it applied. freezeAgent halts every mandate held by that agent; freezePrincipal halts every mandate granted by that principal, including mandates granted while the freeze is in place, so no enumeration of the principal's agents is required. The admin role governing enforcer permissions MUST NOT be the same address as any enforcer.

An approved operator MAY call revokeMandate and extendMandate on behalf of the principal.

recordExecution MUST only be callable by an authorized recorder: the mandate's asset, the principal's account, or an address granted the recorder role by the implementation's access control. Arbitrary callers MUST be rejected. recordExecution MUST apply the same existence, validity-window, revocation, action, and agent and principal freeze checks as canExecute and revert if any fail. The asset is not a parameter of recordExecution; the caller restriction binds it. recordExecution increments cumulativeUsed by amount and MUST revert if amount exceeds maxTransactionValue or if amount exceeds maxCumulativeValue - cumulativeUsed (when either is not set to type(uint256).max). cumulativeUsed MUST NOT reset on extendMandate. A cap reset requires explicit revocation and re-issuance. cumulativeUsed sums the recorded amount of every action, so it reflects recorded authority rather than net tokens moved; for example an approval, or a direct call by the principal, advances it with no matching transfer.

Revocation MUST NOT delete the mandate record: revokeMandate sets revoked to true and leaves the record in place.

grantMandate MUST emit MandateGranted and one ActionEnabled per enabled action, revokeMandate MUST emit MandateRevoked, extendMandate MUST emit MandateExtended, setOperator MUST emit OperatorSet, recordExecution MUST emit ExecutionRecorded, freezeAgent and unfreezeAgent MUST emit AgentFrozen and AgentUnfrozen, and freezePrincipal and unfreezePrincipal MUST emit PrincipalFrozen and PrincipalUnfrozen. Nothing is emitted for the actions a new mandate clears, so consumers reading logs MUST treat MandateGranted as resetting the action set for the pair.

Signed lifecycle operations

grantMandate, revokeMandate, extendMandate, and setOperator MAY be called directly by the principal (msg.sender == principal) or by any submitter providing a principal signature over an EIP-712 typed message. Implementations MUST verify principal signatures using ERC-1271-compatible verification. The reference path is SignatureChecker.isValidSignatureNow(principal, digest, signature), which handles EOAs, contract wallets (multisigs, DAOs), and EIP-7702-delegated accounts.

The typed-data domain separator follows EIP-712 with name = "RAMS", version = "1", current chainId, and verifyingContract set to the RAMS registry address. Implementations MUST track a nonce per principal (nonces[principal]) and include it in every signed operation; each signed operation MUST use a distinct nonce. Implementations MUST revert if block.timestamp > deadline or if the recovered signer does not match principal.

The normative EIP-712 typehashes are:

bytes32 constant GRANT_MANDATE_TYPEHASH = keccak256(
    "GrantMandate(address agent,uint48 validFrom,uint48 validUntil,"
    "address principal,address complianceProvider,bytes32 identityRef,"
    "address asset,uint256 maxTransactionValue,uint256 maxCumulativeValue,"
    "bytes32 metadata,bytes32[] actions,uint256 nonce,uint256 deadline)"
);

bytes32 constant REVOKE_MANDATE_TYPEHASH = keccak256(
    "RevokeMandate(address agent,address principal,uint256 nonce,uint256 deadline)"
);

bytes32 constant EXTEND_MANDATE_TYPEHASH = keccak256(
    "ExtendMandate(address agent,address principal,uint48 newValidUntil,uint256 nonce,uint256 deadline)"
);

bytes32 constant SET_OPERATOR_TYPEHASH = keccak256(
    "SetOperator(address principal,address operator,bool approved,uint256 nonce,uint256 deadline)"
);

grantMandate MUST revert if the (agent, principal) pair already has an active mandate. grantMandate MUST revert if complianceProvider is address(0), and MUST revert if complianceProvider.checkPrincipal returns eligible == false. extendMandate MUST revert if newValidUntil is less than or equal to the current validUntil. When checkPrincipal returns a nonzero expiresAt, grantMandate and extendMandate MUST revert if validUntil (respectively newValidUntil) is later than that expiresAt, so a mandate cannot outlive the principal's compliance window. extendMandate MUST re-check checkPrincipal and revert if the principal is no longer eligible. grantMandate MUST revert if validUntil is not greater than validFrom, and MUST revert if validUntil is not greater than the current block timestamp, so a mandate cannot be granted already expired or permanently unusable. A compliance provider returning eligible == true MUST NOT return an expiresAt in the past, and grantMandate MUST revert if it does. grantMandate MUST revert if any element of actions is bytes32(0), so an unset label cannot become an enabled action.

At grantMandate the implementation writes the signed actions[] array into the actionEnabled mapping keyed by (agent, principal). It MUST first clear any actions enabled by a prior mandate for the pair, and the new mandate MUST start with revoked set to false and cumulativeUsed set to 0. Subsequent on-chain enforcement reads from this mapping in O(1).

Since actionEnabled cannot be enumerated and events are not readable on-chain, that clear requires the enabled labels of the current mandate to be recorded enumerably: implementations MUST keep that set enumerable on-chain, for example as an array per (agent, principal), as the reference implementation does, or by keying the mapping on a per-grant generation counter. If the clear is skipped, the old actions stay enabled, so a principal who re-issues a narrower mandate keeps the wider mandate's actions.

Authorization check

canExecute MUST behave as follows (Solidity pseudocode, illustrative):

function canExecute(
    address agent,
    address principal,
    address asset,
    bytes32 action,
    uint256 amount
) public view returns (bool ok, MandateReason reason) {
    Mandate storage m = mandates[agent][principal];

    if (m.principal == address(0))                           return (false, MandateReason.NONEXISTENT);
    if (isAgentFrozen(agent))                                return (false, MandateReason.AGENT_FROZEN);
    if (isPrincipalFrozen(principal))                        return (false, MandateReason.PRINCIPAL_FROZEN);
    if (asset != m.asset)                                    return (false, MandateReason.WRONG_ASSET);
    if (block.timestamp < m.validFrom)                       return (false, MandateReason.NOT_YET_VALID);
    if (block.timestamp > m.validUntil)                      return (false, MandateReason.EXPIRED);
    if (m.revoked)                                           return (false, MandateReason.REVOKED);
    if (!actionEnabled[agent][principal][action])            return (false, MandateReason.ACTION_NOT_ENABLED);

    if (m.maxTransactionValue != type(uint256).max
        && amount > m.maxTransactionValue)                   return (false, MandateReason.OVER_TX_CAP);

    if (m.maxCumulativeValue != type(uint256).max
        && amount > m.maxCumulativeValue - m.cumulativeUsed) return (false, MandateReason.OVER_CUMULATIVE_CAP);

    return (true, MandateReason.OK);
}

canExecute returns a boolean and a MandateReason. The reason is OK when the boolean is true. When it is false, reason MUST be the first failing check in the order shown above, so an integrator can tell an expired mandate from an over-cap amount and respond accordingly. Reasons MUST be appended without renumbering the existing values, so the order of the enum values carries no meaning and MUST NOT be read as the order the checks run in. AGENT_FROZEN and PRINCIPAL_FROZEN are evaluated before the mandate-specific checks because a freeze applies to every mandate the frozen party holds rather than to one mandate, and an enforcement freeze would otherwise be masked by an unrelated failing check on a single mandate. Only NONEXISTENT precedes them, since a pair with no mandate has nothing to halt. The precedence runs one way: a freeze reported for a mandate that is also expired or revoked leaves that reason to surface once the freeze is lifted. The listed reasons are the normative set; OTHER is reserved for implementation-specific checks not covered here and MUST NOT stand in for a listed reason. A registry that cannot evaluate a check MUST revert rather than return ok == false, so a failed evaluation is never reported as a denial. The registry holds no funds and cannot evaluate balances, allowances, or custody, and MUST NOT return OTHER for those conditions.

Interface

interface IAgentMandate is IERC165 {

    enum MandateReason {
        OK,
        NONEXISTENT,
        WRONG_ASSET,
        NOT_YET_VALID,
        EXPIRED,
        REVOKED,
        ACTION_NOT_ENABLED,
        AGENT_FROZEN,
        PRINCIPAL_FROZEN,
        OVER_TX_CAP,
        OVER_CUMULATIVE_CAP,
        OTHER
    }

    struct Mandate {
        address agent;
        uint48  validFrom;
        uint48  validUntil;
        address principal;
        bool    revoked;
        address complianceProvider;
        bytes32 identityRef;
        address asset;
        uint256 maxTransactionValue;
        uint256 maxCumulativeValue;
        uint256 cumulativeUsed;
        bytes32 metadata;
    }

    /// @notice Emitted when a mandate is granted.
    event MandateGranted(
        address indexed agent,
        address indexed principal,
        address complianceProvider,
        address asset,
        uint48 validFrom,
        uint48 validUntil,
        bytes32 metadata
    );

    /// @notice Emitted when an action is enabled on a mandate at grant time.
    event ActionEnabled(address indexed agent, address indexed principal, bytes32 indexed action);

    /// @notice Emitted when a mandate is revoked.
    event MandateRevoked(
        address indexed agent,
        address indexed principal,
        address revokedBy
    );

    /// @notice Emitted when a mandate's validity is extended.
    event MandateExtended(
        address indexed agent,
        address indexed principal,
        uint48 newValidUntil
    );

    /// @notice Emitted when an operator approval is set or revoked.
    event OperatorSet(
        address indexed principal,
        address indexed operator,
        bool approved
    );

    /// @notice Emitted when an agent executes an action recorded by a RAMS-aware token.
    event ExecutionRecorded(
        address indexed agent,
        address indexed principal,
        bytes32 indexed action,
        uint256 amount,
        uint256 cumulativeUsed
    );

    /// @notice Emitted when an agent is frozen. Freezing is restricted to authorized enforcer roles.
    event AgentFrozen(
        address indexed agent,
        address indexed enforcer
    );

    /// @notice Emitted when a freeze is lifted.
    event AgentUnfrozen(
        address indexed agent,
        address indexed enforcer
    );

    /// @notice Emitted when a principal is frozen. Freezing is restricted to authorized enforcer roles.
    event PrincipalFrozen(
        address indexed principal,
        address indexed enforcer
    );

    /// @notice Emitted when a freeze on a principal is lifted.
    event PrincipalUnfrozen(
        address indexed principal,
        address indexed enforcer
    );

    /// @notice Parameters for grantMandate, bundled into a struct to avoid stack-too-deep.
    /// @param agent The address of the agent receiving the mandate.
    /// @param validFrom Unix timestamp from which the mandate is active.
    /// @param validUntil Unix timestamp after which the mandate expires.
    /// @param principal The address of the principal granting the mandate.
    /// @param complianceProvider Address of an IComplianceProvider. MUST be a non-zero address.
    /// @param identityRef Off-chain identity reference for the principal.
    /// @param asset Specific asset address.
    /// @param maxTransactionValue Per-transaction value cap.
    /// @param maxCumulativeValue Cumulative value cap over the mandate's lifetime.
    /// @param metadata Optional 32-byte pointer to off-chain metadata (e.g., legal-text content hash).
    /// @param actions Array of action labels.
    /// @param deadline Signature expiry timestamp (replay protection).
    struct GrantMandateParams {
        address   agent;
        uint48    validFrom;
        uint48    validUntil;
        address   principal;
        address   complianceProvider;
        bytes32   identityRef;
        address   asset;
        uint256   maxTransactionValue;
        uint256   maxCumulativeValue;
        bytes32   metadata;
        bytes32[] actions;
        uint256   deadline;
    }

    /// @notice Grants a mandate from a principal to an agent.
    /// @dev If `signature` is empty, msg.sender MUST equal params.principal. Otherwise the signature is verified
    ///      via SignatureChecker against the GrantMandate [EIP-712](/eips/eip-712.html) digest.
    /// @param params The mandate parameters.
    /// @param signature Principal signature ([EIP-712](/eips/eip-712.html), [ERC-1271](/eips/eip-1271.html) supported).
    function grantMandate(GrantMandateParams calldata params, bytes calldata signature) external;

    /// @notice Revokes the active mandate for the given agent and principal.
    /// @dev If `signature` is empty, msg.sender MUST be the principal or an approved operator. Otherwise the
    ///      signature is verified via SignatureChecker against the RevokeMandate [EIP-712](/eips/eip-712.html) digest.
    /// @param agent The agent address whose mandate is revoked.
    /// @param principal The principal address whose mandate is revoked.
    /// @param deadline Signature expiry timestamp.
    /// @param signature Principal signature ([EIP-712](/eips/eip-712.html), [ERC-1271](/eips/eip-1271.html) supported).
    function revokeMandate(
        address agent,
        address principal,
        uint256 deadline,
        bytes calldata signature
    ) external;

    /// @notice Extends the validity of an existing mandate without resetting cumulativeUsed.
    /// @dev If `signature` is empty, msg.sender MUST be the principal or an approved operator. Otherwise the
    ///      signature is verified via SignatureChecker against the ExtendMandate [EIP-712](/eips/eip-712.html) digest.
    /// @param agent The agent address.
    /// @param principal The principal address.
    /// @param newValidUntil New expiry timestamp. MUST be greater than the current validUntil.
    /// @param deadline Signature expiry timestamp.
    /// @param signature Principal signature ([EIP-712](/eips/eip-712.html), [ERC-1271](/eips/eip-1271.html) supported).
    function extendMandate(
        address agent,
        address principal,
        uint48 newValidUntil,
        uint256 deadline,
        bytes calldata signature
    ) external;

    /// @notice Freezes an agent, halting all of its mandates. Restricted to authorized enforcer roles.
    /// @param agent The agent address to freeze.
    function freezeAgent(address agent) external;

    /// @notice Lifts a freeze on an agent.
    /// @param agent The agent address to unfreeze.
    function unfreezeAgent(address agent) external;

    /// @notice Freezes a principal, halting every mandate granted by that principal.
    /// @param principal The principal address to freeze.
    function freezePrincipal(address principal) external;

    /// @notice Lifts a freeze on a principal.
    /// @param principal The principal address to unfreeze.
    function unfreezePrincipal(address principal) external;

    /// @notice Sets or revokes operator approval for the principal.
    /// @dev Callable by the principal directly or by anyone with a valid principal signature.
    /// @param principal The principal granting/revoking operator status.
    /// @param operator The operator address being approved or revoked.
    /// @param approved True to approve, false to revoke.
    /// @param deadline Signature expiry timestamp.
    /// @param signature Principal signature ([EIP-712](/eips/eip-712.html), [ERC-1271](/eips/eip-1271.html) supported).
    function setOperator(
        address principal,
        address operator,
        bool approved,
        uint256 deadline,
        bytes calldata signature
    ) external;

    /// @notice Records an agent-initiated execution. Called by RAMS-aware regulated tokens.
    /// @param agent The agent address.
    /// @param principal The principal on whose behalf the action is executed.
    /// @param action The action label being executed.
    /// @param amount The amount in the asset's base unit.
    function recordExecution(
        address agent,
        address principal,
        bytes32 action,
        uint256 amount
    ) external;

    /// @notice Returns whether the agent can execute the action on the asset for the principal at the given
    ///         amount, and the reason.
    /// @dev Bundles asset, existence, validity, agent and principal freeze, action, and cap checks into one call.
    /// @param agent The agent address.
    /// @param principal The principal address.
    /// @param asset The asset the action targets; MUST equal the mandate's `asset`.
    /// @param action The action label being checked.
    /// @param amount The amount to check, in the asset's base unit.
    /// @return ok True if the agent can execute the action at this amount.
    /// @return reason MandateReason.OK when ok is true, otherwise the first failing check.
    function canExecute(
        address agent,
        address principal,
        address asset,
        bytes32 action,
        uint256 amount
    ) external view returns (bool ok, MandateReason reason);

    /// @notice Returns true if the action is enabled on the mandate.
    /// @param agent The agent address.
    /// @param principal The principal address.
    /// @param action The action label.
    /// @return True if the action is enabled.
    function isActionEnabled(address agent, address principal, bytes32 action) external view returns (bool);

    /// @notice Returns the full Mandate struct for the given agent and principal.
    /// @param agent The agent address.
    /// @param principal The principal address.
    /// @return The Mandate struct.
    function getMandate(address agent, address principal) external view returns (Mandate memory);

    /// @notice Returns true if the operator is approved for the given principal.
    /// @param principal The principal address.
    /// @param operator The operator address.
    /// @return True if approved.
    function isOperator(address principal, address operator) external view returns (bool);

    /// @notice Returns true if the agent is frozen.
    /// @param agent The agent address.
    /// @return True if frozen.
    function isAgentFrozen(address agent) external view returns (bool);

    /// @notice Returns true if the principal is frozen.
    /// @param principal The principal address.
    /// @return True if frozen.
    function isPrincipalFrozen(address principal) external view returns (bool);

    /// @notice Returns the current nonce for a principal (used in signed operations).
    /// @param principal The principal address.
    /// @return The current nonce value.
    function nonces(address principal) external view returns (uint256);

    /// @notice Returns the [EIP-712](/eips/eip-712.html) domain separator.
    /// @return The domain separator hash.
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}

Integration with Regulated Token Contracts

A regulated token integrates RAMS by gating each function it lets an agent perform for a holder (for standards such as ERC-7943 and ERC-3643). The principal is the holder (from) and the agent is the caller (msg.sender); a call with msg.sender != from is agent-initiated and MUST satisfy the (msg.sender, from) mandate.

RAMS mandate validity does NOT replace token-level allowance or operator approval. For an agent-initiated transfer to succeed, both the token-level authorization (ERC-20 allowance, or ERC-721/ERC-1155 operator approval) and the RAMS mandate MUST pass. RAMS is an additional compliance layer, not a replacement for token ownership semantics.

The action label is an opaque bytes32 chosen by the token: a function selector, a keccak256 of a string, or any other 32-byte identifier matching the labels the principal signed. A selector MUST be left-aligned as bytes32(selector), so the label the principal signs and the label the token computes are the same value.

Each gated function carries its own label, as a compile-time constant:

function transferFrom(address from, address to, uint256 value)
    public override
    gatedByMandate(IERC20.transferFrom.selector, from, value)
    returns (bool)
{
    return super.transferFrom(from, to, value);
}

gatedByMandate is in the Reference Implementation. A token MAY write its own gate.

This example is strict: any caller that is not the holder needs a mandate, so an ordinary ERC-20 spender with only an allowance is rejected with NONEXISTENT. A token MAY instead check only callers that hold a mandate, leaving normal allowance transfers untouched.

The enforcement venue depends on whether the asset itself can be changed:

  1. Token gate: a new or upgradeable token gates its own functions, as above.
  2. EIP-7702 account: the principal delegates their account to an IAgentExecutor, so agent actions originate as the principal.
  3. Executor: the principal approves an IAgentExecutor and the agent acts only through it.

Venues 2 and 3 exist because an asset already deployed without RAMS awareness cannot be gated. All three read the same mandate, so the agent's authority does not depend on which one applies. They do not combine: a gated token evaluates msg.sender, so a call forwarded by an executor is evaluated against the executor rather than the agent. The venue is out of scope of the normative interface.

The example above is principal-custodied, the known pattern for regulated assets, but RAMS is custody-agnostic: an agent MAY hold the asset itself when its own wallet is an eligible holder, in which case the token's eligibility check already governs it.

Rationale

RAMS is a separate ERC rather than an extension of any agent identity or token compliance standard because mandate delegation is a distinct concern. Coupling it to a specific standard would limit its use across the fragmented regulated token ecosystem.

Mandate storage is keyed by (agent, principal) addresses, similar to ERC-20 allowance.

A single IComplianceProvider interface is used rather than separate identity and compliance interfaces because identity verification is a logical subset of compliance checking. A compliance provider that declares a principal eligible has already verified that the underlying identity is valid and unrevoked.

Mandate scope is encoded fully on-chain and bound by the principal's EIP-712 signature, eliminating drift between on-chain state and any off-chain document. Actions are signed as a bytes32[] array and written into a mapping at grant time for O(1) enforcement. The metadata field is non-normative and does not participate in enforcement.

All signed lifecycle operations include a nonce and deadline; ERC-1271 verification allows smart-wallet principals (multisigs, DAOs).

canExecute bundles every runtime check into one call so the integrator cannot accidentally skip one.

canExecute does not call the compliance provider. It sits in the transfer path of every gated asset, so an external call there would let a provider that reverts, is upgraded badly, or becomes unreachable halt all activity on assets that merely reference it. Eligibility is therefore checked when a mandate is granted or extended, and the mandate's validUntil is bounded under the principal's expiresAt so it cannot outlive the compliance window it was issued against. Eligibility lost inside that window is handled by the enforcer freeze and by the asset's own compliance checks, not by the registry.

canExecute returns a MandateReason rather than a bare boolean so an integrator knows what to do next. The reasons group into five responses: NOT_YET_VALID means wait; WRONG_ASSET and OVER_TX_CAP succeed on a retry with different parameters; NONEXISTENT, EXPIRED, REVOKED, ACTION_NOT_ENABLED and OVER_CUMULATIVE_CAP need the principal to grant, extend or widen a mandate, and a spent cumulative budget is not restored by extendMandate; AGENT_FROZEN and PRINCIPAL_FROZEN need an enforcer; OTHER carries no remediation and an integrator cannot infer one.

Caps are per mandate, so a principal granting several mandates is not bounded in aggregate by this standard; each principal manages its own total exposure.

Freeze authority is kept within IAgentMandate rather than a separate registry because an enforcer does not exist independently of the mandates it can freeze. Enforcer authority is governed by the implementation's access-control roles, not by a hardcoded tier.

Enforcement is venue-agnostic: RAMS standardizes the mandate, not how it is enforced. Any contract in the agent's path applies it by calling canExecute, so the same mandate is honored whether the gate is in the token, in the principal's account, or in an executor. The account-side venues exist because an asset already deployed cannot be changed to gate itself.

Agents use standard token functions (ERC-20, ERC-721, ERC-1155) rather than agent-prefixed variants because that would require interface duplication. The token's own gate validates mandates via canExecute, requiring no new functions on the token.

Value limits are denominated in token base units rather than fiat to stay deterministic and oracle-free.

cumulativeUsed does not reset on extendMandate because a mandate represents a single delegation agreement; a cap reset requires explicit revocation and re-issuance.

Freezing is reserved for authorized enforcer roles, reflecting the exceptional nature of a halt. It is deliberately coarse: freezeAgent is keyed by agent and halts that agent's mandates from every principal at once, freezePrincipal is keyed by principal and halts every mandate that principal granted, and neither requires enumerating the pairs involved. Enforcement stops at halting. Revocation is left to the principal because a mandate is the principal's own delegation of authority, revocation is irreversible, and restoring a revoked mandate needs a fresh signature from the principal, which is unavailable in exactly the compliance scenarios a freeze exists for. A freeze is the reversible tool an enforcer needs while a lapse is resolved; revocation would let a registry operator permanently unwind delegations, which is the capture risk the model is built to avoid.

Operator permissions are explicitly scoped so that delegation remains auditable: an operator can revoke or extend a mandate but cannot grant new ones.

Backwards Compatibility

RAMS introduces no changes to any existing standard.

Reference Implementation

A reference implementation and test suite are available: the IAgentMandate registry, an IComplianceProvider, an IAgentExecutor, a RamsGated base contract, and an ERC-7943 asset that inherits it.

IAgentExecutor is an OPTIONAL, non-normative companion interface for the account-side enforcement venues (an EIP-7702 delegate or a standalone executor). The forwarding logic is never part of IAgentMandate.

interface IAgentExecutor {
    /// @dev msg.sender is the agent; the implementer is bound to a principal.
    ///      action = bytes4(data), amount read from data, so gated values match the real call.
    ///      Calls canExecute and reverts if false, records the execution, then forwards the call.
    function execute(address target, bytes calldata data) external returns (bytes memory);
}

The executor maintains a mapping from each supported action selector to the position of its amount argument; on execute, it reads the gated amount from that position in the forwarded calldata, so the gated value is always the value that executes. Actions with no value argument gate at amount 0. RAMS gates a registered set of action signatures, not arbitrary calldata. The reference emits an event whenever this registry changes, so the amount-position configuration is auditable off-chain.

RamsGated is an OPTIONAL, non-normative base contract for the token gate venue, holding the registry address as rams. A token inherits it and applies gatedByMandate to each function an agent performs for a holder.

error MandateBlocked(IAgentMandate.MandateReason reason);

modifier gatedByMandate(bytes4 selector, address holder, uint256 amount) {
    bytes32 action = bytes32(selector);
    bool agentCall = msg.sender != holder;

    if (agentCall) {
        (bool ok, IAgentMandate.MandateReason reason) =
            rams.canExecute(msg.sender, holder, address(this), action, amount);
        if (!ok) revert MandateBlocked(reason);
    }

    _;

    if (agentCall) rams.recordExecution(msg.sender, holder, action, amount);
}

Solidity modifiers cannot be overloaded, so a token labelling actions with keccak256 strings rather than selectors writes its own gate.

Security Considerations

identityRef is a reference, not proof of eligibility. Grant-time eligibility comes from checkPrincipal; runtime eligibility comes from the token's own check, on assets that perform one. If an agent wallet is also a standard investor address, the agent-detection logic could misidentify it; agent identity registries should require proof of key control and emit a distinct event when registering an agent wallet.

If recordExecution were callable by arbitrary addresses, an attacker could advance cumulativeUsed to exhaust the cap and deny service to the legitimate agent. The caller restriction specified in the Specification closes this attack surface, leaving the recorder role itself as a trusted surface. Callers can use canExecute for pre-transaction checks or rely on recordExecution's revert behavior for atomic enforcement.

If a revoked mandate's record were removed instead of flagged, then on a token that checks only callers holding a mandate, an agent that still holds a token-level allowance would fall through to plain allowance rules, turning revocation into a silent permission upgrade. Retaining the record also preserves the audit trail.

A compromised compliance provider can approve an ineligible principal at grant time. This is bounded: compliance is checked at grant, not on the execution path, so a provider going offline cannot brick active mandates, though it does block extension; the token's own eligibility check (canSend) still runs on every transfer of an asset that implements one; and an enforcer can freeze the agent or the principal independently of provider state. Principals should select compliance providers with audited, time-locked upgrade mechanisms.

Those two runtime layers are not present in every venue. An asset that carries its own eligibility logic gates each transfer independently of the registry, so a principal who loses eligibility mid-mandate is stopped by the asset even if no enforcer acts. An IAgentExecutor applying a mandate over an asset with no compliance logic of its own, which is the venue this specification provides for assets already deployed and unchangeable, has no such layer: the enforcer freeze is then the only runtime control that responds to eligibility lost inside a mandate's validity window. Deployments of that shape SHOULD treat the freeze relay described below as required rather than optional, and SHOULD issue short mandates renewed through extendMandate, since extension re-checks checkPrincipal and so converts mandate length into the interval at which eligibility is revisited.

A transaction can fail at two distinct compliance layers: the token's investor eligibility check on the principal, or the RAMS mandate validity check on the agent. Frontends and autonomous agents should pre-verify both layers before submitting a transaction to enable clear diagnostic reporting.

A window exists between a PrincipalRevoked event from the compliance provider and enforcement of a freeze on the RAMS registry. High-sensitivity protocols should use an automated freeze relay that monitors PrincipalRevoked events and calls freezePrincipal immediately, which matches the scope of the event, since PrincipalRevoked names a principal and not the agents holding that principal's mandates. The admin role MUST NOT be the same address as any enforcer, preventing self-escalation.

A gate applies only to the functions it is placed on. A function an agent can call for a holder without a gate admits that agent on its token-level allowance alone, with no mandate check, so every such function MUST carry the gate.

Under EIP-7702 the agent's calls originate as the principal, so msg.sender equals the holder and a token gate cannot tell the agent from the principal acting directly. Enforcement for that venue belongs to the IAgentExecutor, which is the only party that knows the acting agent.

An IAgentExecutor reads the gated amount from a registered per-action position. That registry is a trusted surface: a wrong position silently mis-gates caps, so the role that maintains it needs the same care as the enforcer role.

The metadata field is non-normative: implementations and integrators MUST NOT rely on it for enforcement decisions, since its content (e.g., off-chain legal text) is not verifiable on-chain. It is provided as an opaque pointer for human-readable context and audit purposes only.

Copyright

Copyright and related rights waived via CC0.