---
eip: 8273
title: Attestation-Gated Agentic Actions
description: An on-chain Agent Attestation Registry with transaction-scoped authorization via transient storage.
author: Yunan Li, Qingzhi Zha (@rickzha610), Xianrui Qin (@xrqin), Vitto Rivabella (@eversmile12)
discussions-to: https://ethereum-magicians.org/t/erc-8273-attestation-gated-agentic-actions/28617
status: Draft
type: Standards Track
category: ERC
created: 2025-05-26
requires: 165, 1153
---

## Abstract

This ERC defines a standard interface for an **on-chain Agent Attestation Registry**. The registry manages attestations issued by Attestors. Each attestation has a complete lifecycle, issuance and transaction-scoped consumption, and is described by an `AttestationRecord` containing:

- **subjectId / subjectType**: identifies the attested subject, for example `subjectId = agentId` + `subjectType = keccak256("ERC8004_AGENT")`, independently of any specific identity system;
- **capability** + **actionDigest**: two independent axes that identify the authorization scope. `capability` is a coarse-grained capability identifier, such as `keccak256("DEFI_ACCESS_V1")`, and expresses "which class of authorization this is." `actionDigest` is a fine-grained action fingerprint and expresses "which concrete action this is bound to." `actionDigest = 0` denotes capability-only mode, where no concrete action is bound. A non-zero `actionDigest` is derived according to rules agreed between the Attestor and the integrating DApp, and **must** include the target contract, function selector, arguments, and a nonce or attestationId to provide replay protection. Per-chain deployment provides chain isolation; `chainId` need not appear in `actionDigest`.
- **evidenceHash**: optionally references a hash of off-chain evidence.

Attestor is an existing role. It may attest to anything: whether an agent identity is genuine, what capability an agent has, agent reputation, or other claims. The precise semantics are defined by `capability`, and when needed by `actionDigest`; this ERC does not constrain them. The registry records, indexes, and enforces the attestation lifecycle. In the standard atomic path, an authorized Attestor calls the single bundled entry point `attestAndCall`; the registry opens an authorization window in transient storage, executes the action through a specified execution profile, and relies on the EVM to automatically clear the authorization state at the end of the transaction.

This ERC uses an **atomic execution model**: issuance and action execution for each attestation occur within a single transaction. There is no expiration mechanism, and there are no long-lived or session-based attestations. Active authorization state is stored through [EIP-1153](./eip-1153.md) `TSTORE` / `TLOAD` and is automatically cleared at the end of the transaction; no persistent active authorization exists. The authorization window is strictly limited to the transaction in which it is issued, and each attestation expresses its full authorization scope through `capability` + `actionDigest`. When an attestation must bind to a single concrete call, the integrating DApp uses a non-zero `actionDigest` in its query, so the authorization is valid only for the concrete action it computes and cannot be reused for unrelated operations under the same coarse-grained capability.

To ensure the target DApp sees the agent's own wallet as `msg.sender`, this ERC abstracts "how the Registry causes the wallet to initiate the target call" as an **execution profile**. The direct wallet execution profile applies to [ERC-7702](./eip-7702.md) EOAs and AA wallets that support relayer execution: the Registry calls a relayer entry point exposed by the wallet, such as `execute`, and the wallet itself calls the target DApp. The [ERC-4337](./eip-4337.md) UserOperation profile applies to existing 4337 wallets: the Registry calls the EntryPoint and submits a UserOperation already authorized by the agentAA, and the EntryPoint follows the standard `validateUserOp -> execute` path. Atomicity is provided by the single `attestAndCall` entry point together with [EIP-1153](./eip-1153.md) transient storage.

This ERC does not specify how the Attestor evaluates subjects off-chain, what trust source it relies on, or what upper-layer platform architecture it uses. These are defined by concrete integrations, including but not limited to agent identity systems such as [ERC-8004](./eip-8004.md). This ERC only standardizes the on-chain attestation issuance entry point, lifecycle, and query interfaces. Questions such as which Attestors are trustworthy, how evaluation is performed, and what evidence format is used are left to upper-layer protocols or deployers.

## Motivation

### Problem Space

Directly assigning identity to Agents leaves several fundamental problems unresolved. For example, we cannot reliably guarantee that the same Agent always remains behind an [ERC-8004](./eip-8004.md) account. Identity can be assigned, but it is difficult to ensure that it remains bound to the same Agent over time.

The core motivation is not to prove "which Agent this is," but to prove "whether the Agent executing this on-chain operation has the required qualification." This is a subtle but important distinction, and it is the focus of this proposal.

This is particularly important for AI agent systems. When an on-chain transaction claims to be related to an agent, relying parties may need to answer several different questions:

- **Action provenance**: "Was this operation really performed by an agent, or is a human pretending to be one?"
- **Operation authorization**: "Is this agent allowed to perform this class of operation?"
- **Runtime verification**: "Is the agent's execution environment trustworthy?"
- **Compliance audit**: "Which operations were autonomous, and which were human-directed?"

These questions are different, but they share the same structural need: **an attestation record that is bound to a concrete operation and queryable on-chain**. In this design, "queryable on-chain" represents active authorization only within the issuing transaction. After the transaction ends, the persistent record serves only as an audit record.

The current on-chain ecosystem lacks this standardized primitive. An identity NFT can tell you that "this address claims to be an agent," but it cannot tell you anything about a particular operation.

**This ERC provides attestation infrastructure, not attestation semantics.** What exactly is being attested, action provenance, operation authorization, runtime verification, compliance status, or any combination of them, is defined by the Attestor through `capability`, and when needed through `actionDigest`.

### Separation of Concerns: Identity vs. Per-Operation Attestation

Consider an analogy from aviation:

- **Identity** — A pilot's identity document proves who the person is. It is long-lived and independent of any particular flight.
- **Per-operation attestation** — Each flight has its own flight log and dispatch release: who operated it, whether they were authorized, and whether the execution environment met requirements. These are operation-scoped and auditable.

The same separation applies to on-chain agent systems:

| Layer | Question Answered | Corresponding System | Lifecycle |
| --- | --- | --- | --- |
| **Identity** | "Is this an agent? Who controls it?" | [ERC-8004](./eip-8004.md) | Long-lived, persistent |
| **Per-operation attestation** | Any claim defined by an Attestor | **This ERC** | Per-operation, single transaction |

Combining both layers into a single primitive would force impossible tradeoffs: identity that is too short-lived breaks long-term reputation, while per-operation attestations that are too persistent create residual false proofs. This ERC keeps per-operation attestation as a separate layer that composes cleanly with the identity layer.

### Motivating Case 1: Agent Utility Tokens in a DeFi Protocol

**Scenario:** An AI agent autonomously operates in a DeFi liquidity protocol: performing cross-chain arbitrage, providing liquidity, and hedging risk positions. The protocol needs to distribute utility tokens to authenticated agents based on operational performance.

**Problem:** How can the `RewardDistributor` contract verify that the caller is an authenticated agent?

**Solution:** Before issuing an attestation, the Attestor performs an off-chain evaluation of the agent corresponding to the relevant `capability`, and when needed `actionDigest`. For `DEFI_ACCESS_V1`, this evaluation typically includes verifying the agent runtime integrity when necessary, such as through TEE remote attestation; reviewing the agent's operational record according to protocol policy, such as performance, slashing history, and compliance screening; and confirming that the requested operation falls within the policy boundary expressed by the `capability`. The evaluation is performed under the Attestor's own trust assumptions; this ERC does not specify its contents. The result of the evaluation is anchored on-chain through `evidenceHash`, which may be the keccak256 of an audit report, a Merkle root of evaluation items, a TEE attestation quote, or a ZK proof commitment.

After the evaluation passes, the Attestor calls the registry's single atomic bundled entry point `attestAndCall`, completing two phases in the same transaction:

1. `attestAndCall(...)` internally creates a persistent audit record and writes the `attestationId` into transient storage slots keyed by `(wallet, capability, actionDigest)` and `(subjectHash, capability, actionDigest)`.
2. The registry executes the action according to the `executionProfile`: it may call a direct execution entry point on the agent wallet, or it may call the EntryPoint to submit a UserOperation already authorized by the agentAA. The target DApp gates by calling `getActiveAttestationByWallet(msg.sender, capability, actionDigest)`. That function reads from transient storage and reverts if the slot is empty.

Both phases complete within the same transaction. At the end of the transaction, the EVM automatically clears transient storage; the attestation is no longer active and cannot be reused. The persistent `AttestationRecord` remains only as an audit record.

When using the [ERC-4337](./eip-4337.md) UserOperation profile, a typical call chain is:

```text
Attestor
  -> Registry.attestAndCall(profile = ERC4337_USEROP_V1)
      -> TSTORE active attestation
      -> EntryPoint.handleOps([userOp])
          -> agentAA.execute(...)
              -> RewardDistributor.claimReward()
      -> transaction end clears transient storage
```

In this path, `RewardDistributor.claimReward()` is initiated by the agentAA, so the target DApp sees `msg.sender` as the agentAA, not the Registry or an external Multicall contract.

The value of fine-grained `actionDigest` can be illustrated by a constrained swap. Suppose the Attestor approves the action "swap at most 1,000 USDC into WETH and send the output back to the user's Vault." The target call is first encoded as `data = abi.encodeCall(Vault.executeSwap, (USDC, WETH, 1000e6, minOut, userVault, nonce))`, then `actionDigest = keccak256(abi.encode(address(vault), data, nonce))` is computed. The Attestor calls `attestAndCall` with `capability = DEFI_SWAP_V1` and that `actionDigest`. When executing, the `Vault` recomputes the `actionDigest` using the same rule and calls `getActiveAttestationByWallet(msg.sender, DEFI_SWAP_V1, actionDigest)`. If someone changes the calldata to "swap 100,000 USDC into a low-quality token and send it to an attacker address," then even if it still belongs to the broad `DEFI_SWAP_V1` class, the recomputed `actionDigest` differs and the gating query reverts. This prevents an attestation approved for a small reviewed swap from being expanded into an asset-transfer authorization.

### Motivating Case 2: Autonomous Token Issuance by an AI Agent

The same atomic pattern applies to more complex scenarios. An AI agent detects a viral event from real-time internet signals, autonomously decides to issue a meme token, and generates the token name, ticker, image, and complete reasoning process, referred to as the Intent Document.

The key difference in this scenario is how `evidenceHash` is used. The Attestor not only verifies the agent identity, but also hashes the agent's full reasoning, the Intent Document, into `evidenceHash`, so the on-chain attestation also anchors an auditable record of the decision process. The `TokenFactory` contract gates with `getActiveAttestationByWallet(msg.sender, capability, actionDigest)`, and the entire `attestAndCall -> wallet / UserOperation executes mint -> transaction end clears authorization` flow completes in one transaction.

### Atomic Attestation Flow

This ERC uses only one protocol lifecycle model: the **atomic attestation flow**. The two steps, `attestAndCall() -> action`, must complete within a single transaction. Active authorization exists only in transient storage and is automatically cleared by the EVM at the end of the transaction.

After evaluation succeeds, the Attestor directly calls `attestAndCall`. The bundled entry point first writes the transient active attestation, then executes the action through the specified execution profile. Active authorization is limited to the current transaction and therefore cannot serve as a reusable cross-transaction authorization.

An execution profile only determines how the action originates from the agent wallet; it does not change the attestation lifecycle. The direct wallet profile and the [ERC-4337](./eip-4337.md) UserOperation profile must both reuse the same `attestAndCall` entry point.

### Key Terms

This section defines terms used in this ERC.

- **Action** — A set of on-chain operations gated by attestation within a single transaction, such as executing a DeFi swap, minting a token, or calling a privileged protocol function.
- **Attestation** — A registry entry created in the registry by an authorized Attestor; its active state exists only in transient storage within the issuing transaction. An attestation itself is not necessarily a cryptographic proof. It is an on-chain recorded statement issued by an Attestor, and it may reference off-chain cryptographic evidence through `evidenceHash`, such as a ZK proof, TEE remote attestation, signed report, or execution trace commitment.
- **Capability (`capability`)** — A `bytes32` value representing a coarse-grained capability or standard, such as `keccak256("DEFI_ACCESS_V1")`. This is the "which class of authorization" axis.
- **Action digest (`actionDigest`)** — A `bytes32` value representing the concrete authorized action. `actionDigest = 0` means capability-only mode, where the attestation is not bound to any concrete action. When `actionDigest != 0`, it **must** bind the target contract, function selector, arguments, and a nonce or other uniqueness source, such as an attestationId or user-provided salt, to provide replay protection. The concrete derivation rule is agreed between the integrating DApp and the Attestor. The derivation scheme **SHOULD** be encoded into the `capability` namespace (e.g. `keccak256("DEFI_SWAP_V1:scheme=ABI_CALLS_ATTID_V1")`) to avoid silent reverts from rule mismatch across Attestors. This is the "which action is it bound to" axis.
- **Attestor** — An entity authorized to open a transaction-scoped attestation window through `attestAndCall` and select an execution profile to execute an action. Before issuing an attestation on-chain, the Attestor evaluates the subject off-chain under its own trust assumptions. The evaluation depends on the semantics of the selected `capability`, and when needed `actionDigest`: for example, action provenance verification, runtime / TEE proof, operational history review, or policy / compliance checks. This ERC does not specify those semantics. The result may be anchored on-chain through `evidenceHash`. When combined with [ERC-8004](./eip-8004.md), the Attestor is closer to a verifier in a synchronous on-chain gating flow: it does not replace [ERC-8004](./eip-8004.md) identity registration, but converts an evaluation result into a temporarily queryable on-chain attestation within one atomic transaction.
- **Execution Profile** — A set of rules defining how `attestAndCall` causes the target call to originate from the agent wallet. The direct wallet profile can call AA / [ERC-7702](./eip-7702.md) wallets that support relayer execution. The [ERC-4337](./eip-4337.md) UserOperation profile can execute a UserOperation already authorized by the agentAA through the EntryPoint. An execution profile is an internal registry concept and does not enter the authorization identity storage key; DApps gate only by `capability` and `actionDigest`, without knowing which profile was used.
- **UserOperation orchestration** — An execution profile. The Attestor submits a UserOperation already authorized by the agentAA. The UserOperation's `sender` must equal the attested `wallet`, and its `callData` must execute the DApp action covered by `capability` + `actionDigest`. The registry must not treat the Attestor as the agentAA's executor; whether the agentAA authorizes the UserOperation is determined by the EntryPoint calling `validateUserOp`.

This ERC does not prescribe the meaning of any specific `capability`; it is named or derived by the integrating DApp and the Attestor. Attestors may also define composite capabilities, such as `AUTHORIZED_AGENT_ACTION_V1`, covering multiple dimensions.

## Specification

The keywords "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in RFC 2119 / RFC 8174.

### Interfaces

This ERC splits functionality into four interfaces. The MUST / OPTIONAL relationship for implementations is as follows:

| Interface | Purpose | Implementation Requirement |
| --- | --- | --- |
| `IERC8273` (core) | Type definitions, `Attested` event, and read-only lookup functions by ID / tuple | **MUST**: all standard implementations must support it |
| `IERC8273AtomicAttestation` | The only external issuance entry point, `attestAndCall` | **MUST**: this is the issuance path for standard implementations; it is named an "extension" only to separate it structurally from view interfaces |
| `IERC8273ActiveAttestation` | Subject-keyed gating query `getActiveAttestation`, which reverts on absence | **SHOULD**: strongly recommended for implementations that perform on-chain gating |
| `IERC8273WalletAttestation` | Wallet-keyed gating and bool views, `isAttestedAddress` / `getActiveAttestationByWallet` | **SHOULD**: this is the most common family for DApps that gate on `msg.sender` |

Implementations declare supported interfaces through [ERC-165](./eip-165.md). A DApp should first call `supportsInterface` to determine which query family the Registry exposes, then choose the corresponding gating primitive. Although `IERC8273AtomicAttestation` is structurally an "extension," it is the core issuance path of the specification; an implementation that does not support it cannot be considered a complete implementation of this ERC.

Standard implementations **MUST support at least one of `IERC8273ActiveAttestation` and `IERC8273WalletAttestation`**, otherwise the gating requirements in the body of the specification have no standard query surface. Implementations that claim support for the [ERC-8004](./eip-8004.md) integration profile **MUST support `IERC8273WalletAttestation`**, because [ERC-8004](./eip-8004.md) integrations are indexed by agentId / address.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

interface IERC8273 is IERC165 {
    // Active authorization is represented by transient storage, not by this enum.
    // AttestationStatus.None is the default zero-value returned for non-existent
    //      records (e.g. getAttestation(0)); all standard records are written as Recorded.
    // renamed Consumed -> Recorded (active state lives in transient storage).
    enum AttestationStatus { None, Recorded }

    struct SubjectRef {
        uint256 subjectId;
        bytes32 subjectType;
    }

    struct ExecutionRequest {
        bytes32 profileId;      // e.g. AGENT_EXECUTE_V1 or ERC4337_USEROP_V1
        bytes32 actionDigest;   // 0 = capability-only mode; non-zero = action-bound mode
        bytes   data;           // profile-specific execution payload
    }

    struct AttestationRecord {
        uint256 subjectId;
        bytes32 subjectType;
        address attestor;
        bytes32 capability;       // coarse-grained authorization class
        bytes32 actionDigest;     // 0 if capability-only; else specific action digest
        uint64  issuedAt;
        AttestationStatus status; // persistent records are always Recorded
        bytes32 evidenceHash;
        address wallet;
    }

    event Attested(
        uint256 indexed attestationId,
        address indexed wallet,
        bytes32 indexed capability,
        bytes32 actionDigest,
        bytes32 subjectHash,
        address attestor,
        uint256 subjectId,
        bytes32 subjectType,
        bytes32 evidenceHash
    );

    function isAttested(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 actionDigest
    ) external view returns (bool);

    function latestAttestationId(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 actionDigest
    ) external view returns (uint256);

    function getAttestation(uint256 attestationId)
        external view returns (AttestationRecord memory record);
}

interface IERC8273ActiveAttestation is IERC8273 {
    function getActiveAttestation(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 actionDigest
    ) external view returns (AttestationRecord memory record);
}

interface IERC8273WalletAttestation is IERC8273 {
    function isAttestedAddress(
        address wallet,
        bytes32 capability,
        bytes32 actionDigest
    ) external view returns (bool);

    function getActiveAttestationByWallet(
        address wallet,
        bytes32 capability,
        bytes32 actionDigest
    ) external view returns (AttestationRecord memory record);
}

interface IERC8273AtomicAttestation is IERC8273 {
    // The only external issuance entry point.
    function attestAndCall(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 evidenceHash,
        address wallet,
        ExecutionRequest calldata exec
    ) external payable returns (uint256 attestationId, bytes memory result);

}

interface IAgentExecute {
    struct AgentCall {
        address target;
        uint256 value;
        bytes data;
    }

    function executeFromRelayer(
        AgentCall[] calldata calls,
        bytes calldata authData
    ) external payable returns (bytes[] memory results);
}
```

### Core Rules

- `subjectHash = keccak256(abi.encode(subjectId, subjectType))`, used for internal lookup and event fields.
- `capability` is a coarse-grained capability identifier, such as `keccak256("DEFI_ACCESS_V1")`; `actionDigest` is a fine-grained action fingerprint, where `= 0` means capability-only mode. Together they identify the full authorization scope of the attestation. Both the registry write performed by `attestAndCall` and the DApp gating query must use the same `(capability, actionDigest)` pair.
- An attestation is active if and only if, within the transaction in which it is issued, a non-zero `attestationId` exists in a transient storage slot keyed by `keccak256(abi.encode("wallet", wallet, capability, actionDigest))` or `keccak256(abi.encode("subject", subjectHash, capability, actionDigest))`. The EVM automatically clears transient storage at the end of each transaction.
- Persistent `AttestationRecord` entries have `status = Recorded` and serve only as immutable audit records. They do not represent active authorization.
- `attestationId == 0` is reserved as a "non-existent" sentinel value.
- Implementations must declare support for the core interface and any implemented extensions through [ERC-165](./eip-165.md). An extension's `interfaceId` is the XOR of selectors **declared directly** in that extension (excluding inherited), matching Solidity's `type(I).interfaceId`.
- `SubjectRef` is used only as an input parameter type. `AttestationRecord` is used both for storage and return values, and includes subject information, attestation metadata, and wallet binding. `capability` and `actionDigest` together carry the full authorization scope.
- `ExecutionRequest` is the execution profile parameter of `attestAndCall`. `exec.profileId` determines how the action is executed; `exec.actionDigest` is the action fingerprint of the attestation and directly becomes one axis of the storage key; `exec.data` is decoded by the corresponding profile.
- `attestAndCall` must be able to receive native tokens. `msg.value` is the native-token amount attached to the call, not a calldata parameter. Each execution profile must define how `msg.value` is forwarded, used, or rejected. When native-token value affects the authorized action, `exec.actionDigest` must bind that amount.
- **Multiple issuances in the same transaction**: when the same `(wallet, capability, actionDigest)` tuple is issued more than once in the same transaction, the second and subsequent `TSTORE` writes overwrite the `attestationId` in the transient slot, causing that slot to point to the latest attestation. Each `attestAndCall` still creates a new persistent `AttestationRecord` with a distinct `attestationId`, so all issuances are preserved at the audit layer. This ERC does not prohibit such duplicate issuance, but implementations **should** avoid depending on "which record was written into the transient slot" as business logic. When `actionDigest` carries a nonce, this collision naturally does not occur.
- **Capability-only normative constraints**: when `actionDigest == 0`, authorization is bounded only by the Attestor's off-chain evaluation, not by anything the protocol enforces on calldata. Capability-only mode **MUST NOT** be used for bare value transfer, privileged state changes, or any financial operation amplifiable by intra-transaction reentry — those **MUST** use action-bound mode, and the DApp **MUST** add a reentrancy guard (the transient slot stays active for the whole tx; the gate alone does not block reentry).

### Function Specification

Functions are grouped by purpose. **Contracts gating sensitive on-chain operations must use the gating primitives.** **View helpers** are only for non-authoritative read paths such as off-chain indexers and UX display. Mixing these two categories is one of the most common integration errors.

#### Mutators

**`attestAndCall`** (extension `IERC8273AtomicAttestation`) — The only external issuance entry point. **MUST** only be callable by authorized Attestors. Implementations must:

1. Check `wallet != 0`, **`capability != 0`**, and `exec.profileId != 0`, and accept either `exec.actionDigest = 0` (capability-only mode) or a non-zero value (action-bound mode). Rejecting `capability == 0` prevents an uninitialized variable from becoming an attack vector. If `msg.value != 0`, **also** require `actionDigest != 0` so the native amount is bound by `actionDigest`.
2. Write the attestation to persistent storage with `status = Recorded` and emit `Attested`.
3. Use `TSTORE` to write `attestationId` into transient storage slots keyed by `keccak256(abi.encode("subject", subjectHash, capability, exec.actionDigest))` and `keccak256(abi.encode("wallet", wallet, capability, exec.actionDigest))`.
4. Select an execution profile according to `exec.profileId` and execute the action. Execution must cause the target DApp to see the attested `wallet` as `msg.sender`; the concrete mechanism is defined by each profile. See the Execution Profiles section.
5. If the call carries `msg.value`, handle that value according to the execution profile rules. Native tokens must not be allowed to remain silently in the Registry. The [ERC-4337](./eip-4337.md) profile **MUST** reject `msg.value` (`handleOps` is not payable; prefund goes through `EntryPoint.depositTo`).
6. If action execution fails, profile validation fails, or success cannot be confirmed, revert the entire transaction, including the persistent audit record and the transient authorization writes.
7. Return directly after successful execution. Transient storage is automatically cleared at the end of the transaction.

#### Gating Primitives (Recommended for On-Chain Authorization)

These two functions are the **recommended path for on-chain gating**. Each function reads from transient storage using `TLOAD`, returns the active record on success, and reverts when the record is absent. The gated contract receives a record rather than a bool, and failure reverts the whole transaction, avoiding integration bugs such as forgetting to check a bool or mishandling an `if` branch. Integrators are still responsible for reentrancy protection inside the gated operation; see Security Considerations.

**`getActiveAttestation(subject, capability, actionDigest)`** (extension `IERC8273ActiveAttestation`) — Reads from transient storage using `TLOAD(keccak256(abi.encode("subject", subjectHash, capability, actionDigest)))`. If the transient slot is non-zero, returns the `AttestationRecord` from persistent storage; if the slot is zero, must revert. `actionDigest = 0` is used for capability-only mode queries.

**`getActiveAttestationByWallet(wallet, capability, actionDigest)`** (extension `IERC8273WalletAttestation`) — Reads from transient storage using `TLOAD(keccak256(abi.encode("wallet", wallet, capability, actionDigest)))`. If the transient slot is non-zero, returns the `AttestationRecord` from persistent storage; if the slot is zero, must revert. This is the recommended primitive when a contract gates on `msg.sender`, as in Motivating Case 1. Capability-only mode queries pass `actionDigest = 0`; action-bound mode queries pass the recomputed concrete digest.

#### View Helpers (For Off-Chain Use Only; Must Not Be Used for Gating)

These functions return bool snapshots of the registry's transient storage state in the current transaction. They are useful for off-chain indexing, UI display, and read-only tooling. They **must not** be the sole gate for sensitive on-chain operations.

**`isAttested(subject, capability, actionDigest)`** — Returns `true` if the transient slot corresponding to `(subjectHash, capability, actionDigest)` is non-zero in the current transaction.

**`isAttestedAddress(wallet, capability, actionDigest)`** (extension `IERC8273WalletAttestation`) — Returns `true` if the transient slot corresponding to `(wallet, capability, actionDigest)` is non-zero in the current transaction. Wallet bindings for different `(capability, actionDigest)` pairs are independent.

**`latestAttestationId(subject, capability, actionDigest)`** — Returns the ID of the most recent persistent attestation under the same `(subject, capability, actionDigest)`; returns `0` if none exists. This value reflects audit records, not active authorization. In action-bound mode, most queries return `0` or a unique ID because `actionDigest` usually includes a nonce.

**`getAttestation(attestationId)`** — Looks up an attestation record by ID from persistent storage and returns the full `AttestationRecord`. All standard records have `status = Recorded`.

#### Non-Standard Helper

**`nextAttestationId()`** — Not part of the standard interface. Returns the ID that will be used by the next attestation, for off-chain batch construction. Implementations may optionally provide it.

### Execution Profiles

An execution profile defines how `attestAndCall` causes the target action to originate from the agent wallet. All profiles must satisfy the same lifecycle constraint: first write the transient active attestation, then execute the action; if execution fails, revert the entire transaction; at transaction end, active attestation is automatically cleared.

#### Direct Wallet Execution Profile

`AGENT_EXECUTE_V1 = keccak256("ERC8273_AGENT_EXECUTE_V1")`.

This profile applies to AA or [ERC-7702](./eip-7702.md) smart wallets that implement `IAgentExecute`. The Registry calls `wallet.executeFromRelayer(calls, authData)`, and the wallet internally calls each `calls[i].target`, so the target DApp sees `msg.sender == wallet`. `exec.data` should be encoded as:

```solidity
abi.encode(IAgentExecute.AgentCall[] calls, bytes authData)
```

Implementations must verify:

- When `exec.actionDigest != 0`: the reference implementation's minimal form is `exec.actionDigest == keccak256(abi.encode(calls, attestationId))` — `attestationId` is allocated by the Registry at issuance time and is naturally per-attestation unique, satisfying the global MUST in the Security section "`actionDigest` derivation" (a non-zero actionDigest must include a nonce or equivalent uniqueness source). Integrators MAY adopt stronger formulas that explicitly include a user-supplied nonce, session salt, or additional bindings; they **must not weaken** the form — a bare `keccak256(calls)` is only compliant when `calls[i].data` already carries a nonce internally and is not recommended as the default.
- When `exec.actionDigest == 0`: the profile still executes `calls`, but the authorization is capability-only, and the DApp side can only query in capability-only mode.
- `wallet != address(0)`;
- `IAgentExecute(wallet).executeFromRelayer(calls, authData)` returns successfully;
- `authData` is verified by the agent wallet, and must bind chainId, wallet, registry, profile, actionDigest, nonce, and validity period;
- If `calls` includes a non-zero `AgentCall.value`, that value must be covered by `exec.actionDigest` in action-bound mode. If ETH enters through `attestAndCall`, the Registry must forward `msg.value` to the wallet's execution entry (Direct Wallet profile only — the [ERC-4337](./eip-4337.md) profile **MUST** reject non-zero `msg.value`; see below).

This profile does not require all AA wallets to expose arbitrary external `execute`. Only wallets that explicitly implement `IAgentExecute` and can use `authData` for replay protection, domain separation, and call binding should claim support for this profile.

#### ERC-4337 UserOperation Profile

`ERC4337_USEROP_V1 = keccak256("ERC8273_ERC4337_USEROP_V1")`.

This profile applies to existing [ERC-4337](./eip-4337.md) AA wallets. In this ERC, UserOperation execution is only one execution profile of `attestAndCall`; it does not introduce a second issuance entry point.

The concrete encoding of `exec.data` may be defined by an implementation or adapter, but it must bind at least:

- `entryPoint`;
- the submitted UserOperation or `handleOps` calldata;
- the UserOperation's `sender`;
- adapter / receipt / postcondition data sufficient to confirm successful execution of the target action.

Implementations must verify:

- the UserOperation's `sender == wallet`;
- the UserOperation is authorized by the agentAA's own nonce, signature, session key, or module policy;
- the target action covered by the UserOperation is consistent with `exec.actionDigest` in action-bound mode;
- success of the target action must not be inferred solely from the fact that the low-level call to `EntryPoint.handleOps` did not revert. If the EntryPoint or account implementation may record UserOperation failure as an event rather than bubbling a revert, the profile adapter must confirm success through an account receipt, DApp receipt, event proof, or other verifiable postcondition; otherwise it must revert.

The value of this profile is compatibility with existing AA wallets that do not want to expose arbitrary external `executeFromRelayer`. The EntryPoint follows the standard `validateUserOp -> execute` path, and the agentAA calls the DApp from its `execute`, so at the target DApp `msg.sender == agentAA`, which equals the attested `wallet`. `getActiveAttestationByWallet(msg.sender, capability, actionDigest)` gates on that basis.

### Wallet Binding

The `wallet` parameter has two purposes: it is the agent wallet that the target action is expected to represent, and it is the key used to write the transient authorization slot `keccak256(abi.encode("wallet", wallet, capability, actionDigest))`.

- `wallet == address(0)`: `attestAndCall` must reject this case. The zero address is not a valid wallet binding and cannot be used as a gating subject. Accepting the zero address would make any query path that failed to bind `wallet` before calling an attack vector.
- `wallet != 0`: implementations must use `TSTORE` to write the transient slot keyed by `keccak256(abi.encode("wallet", wallet, capability, actionDigest))`. During the transaction, `getActiveAttestationByWallet(wallet, capability, actionDigest)` reads this slot using `TLOAD` and returns the record. After the transaction ends, the EVM automatically clears the slot.
- Wallet binding is scoped by the transient slot `(wallet, capability, actionDigest)`. Different `(wallet, capability, actionDigest)` tuples are independent and are all automatically cleared at the end of the transaction.
- `wallet` must appear in the `Attested` event and should be an indexed topic to enable wallet-dimension indexing.

### `capability`, `actionDigest`, and `evidenceHash`

**`capability`** expresses a coarse-grained authorization class, such as `keccak256("DEFI_ACCESS_V1")` or `keccak256("MINT_AUTHORITY_V1")`. `capability` is a flat namespace whose semantics are agreed by the integrating DApp and the Attestor.

**`actionDigest`** expresses fine-grained action binding: "which concrete action this is bound to." The rules are:

- `actionDigest = 0`: **capability-only mode**. The attestation is not bound to any concrete action; the DApp gates with `getActive...(..., capability, 0)`.
- `actionDigest != 0`: **action-bound mode**. The attestation is bound to a concrete action. The Attestor and integrating DApp must agree on a derivation rule, and the `actionDigest` inputs **must** include the target contract, function selector, arguments, and a nonce or other uniqueness source, such as an attestationId or user-provided salt, to provide replay protection. When the action carries native-token value, the value must also be included in `actionDigest`.

**`chainid` is not included in any derivation**: the Registry is deployed per chain, and storage is naturally chain-isolated, which structurally provides cross-chain replay protection. **`exec.profileId` is not included in any derivation**: the execution profile is an internal Registry concept that the DApp does not observe. If a particular DApp truly needs profile binding, it may explicitly include it in `actionDigest` derivation, but this is an exceptional choice, not the default.

**A mismatch between the Attestor's and DApp's derivation rules is a critical integration error**. It causes the Attestor to attest under `(capability, digestA)` while the DApp queries `(capability, digestB)`, so the transient slot is not found and the gating query reverts. This is not a vulnerability, but it is a deployment error and must be covered by tests.

`evidenceHash` may point to arbitrary off-chain evidence: review reports, execution trace hashes, TEE remote attestation, signed verification reports, and so on. This ERC only standardizes the `bytes32` commitment itself. Evidence storage, transport, and format are defined by upper layers. Evidence references should use content addressing, such as an IPFS CID or signed Merkle root, so the commitment is not weakened by mutable references.

## Rationale

### Design Decisions

- **Generic `SubjectRef` instead of address**: Attestation needs are broader than any single identity scheme. A generic subject reference lets this ERC adapt to future identity systems without rewriting the standard. `SubjectRef` is used as an input parameter, and its fields are persisted in `AttestationRecord`, so callers do not need to manage two separate return values.
- **`bytes32 evidenceHash` instead of embedded metadata**: Evidence may be large, private, or mutable. A hash commitment keeps the interface compact and supports many off-chain storage systems.
- **`capability` + `actionDigest` axes instead of derived `scopeId`**: Earlier versions derived a single `scopeId` by hashing capability, wallet, chainid, profileId, and actionDigest. That design cannot distinguish "coarse-grained capability" from "action-bound fingerprint" at the type level: both are `bytes32`, and misconfiguration can fail silently. This ERC splits those semantic dimensions into independent axes: `capability` is the coarse-grained class; `actionDigest` is the fine-grained action binding, with `= 0` denoting capability-only mode. The interface signature forces the mode choice. `wallet` is already a separate function parameter, so repeating it in a derivation is redundant. Per-chain Registry deployment provides chain isolation; `chainId` need not appear in `actionDigest`. `profileId` is an internal Registry concept and should not leak into the DApp interface.
- **No `update` / `batch`**: In-place modification blurs history and complicates the authorization window. The atomic lifecycle only needs one `attestAndCall` per batch; more complex compositions should be expressed inside execution profiles, such as direct wallet calls or [ERC-4337](./eip-4337.md) UserOperations.
- **Transient storage for active authorization**: [EIP-1153](./eip-1153.md) transient storage is automatically cleared at the end of each transaction. Active authorization therefore never enters persistent on-chain state, and the authorization window is structurally limited to a single transaction. The atomic model enforces short lifetimes through the EVM rather than operational discipline, preventing reusable authorization from remaining after transaction end.
- **`attestAndCall` as the only external issuance entry point**: Issuance, transient authorization writes, and action execution are orchestrated by one entry point, ensuring that each active attestation is used for a corresponding action in the same call stack and that the authorization window is limited to a single transaction.
- **Unified `AttestationRecord`**: Subject information (`subjectId`, `subjectType`) and wallet binding (`wallet`) are included in `AttestationRecord`, so the interface and implementation share one struct and no internal conversion layer is needed. `SubjectRef` remains as an input parameter type to preserve semantic grouping.
- **Wallet binding isolated by `(capability, actionDigest)`**: Wallet binding uses `keccak256(abi.encode("wallet", wallet, capability, actionDigest))` as the transient storage slot key. This prevents cross-capability interference, and it also prevents action-bound and capability-only modes under the same capability from satisfying each other.
- **Split between gating primitives and view helpers**: This ERC intentionally splits `getActiveAttestation*`, which reverts on absence, from `isAttested*`, which returns a bool snapshot. Both read from transient storage. The reverting variant is the recommended path for on-chain authorization because it forces the transaction to revert when the attestation is absent, eliminating common errors such as forgetting to check a bool or mishandling an `if` branch. The wallet-keyed variant `getActiveAttestationByWallet(wallet, capability, actionDigest)` ensures that the most common gating form also has a safe path without falling back to bool views.
- **Execution profiles instead of a single wallet assumption**: This ERC does not hardcode `IAgentExecute` or [ERC-4337](./eip-4337.md) as the only execution mechanism. The direct wallet profile applies to agent wallets willing to expose relayer execution; the [ERC-4337](./eip-4337.md) UserOperation profile applies to existing AA wallets. Both share the same transient attestation lifecycle.

### Relationship with Existing Attestation Systems

- **Ethereum Attestation Service (EAS)**: EAS is a schema-driven singleton deployment suited for broad ecosystem-level attestation use cases, and by default uses time-based expiration and persistent storage. An EAS resolver can execute custom logic in `onAttest` / `onRevoke`, so it can cover part of this ERC's design space. However, that means every schema / resolver must define its own data format, execution method, and query functions, and resolvers written by different projects may not be compatible. By contrast, this ERC standardizes a fixed flow: attestations can point to non-address subjects, bind to a specific wallet, let an Attestor trigger transaction-scoped authorization and action execution through a uniform entry point, let DApps query the current transaction's authorization through uniform functions, and automatically clear authorization at transaction end. EAS resolvers can approximate this flow for project-specific needs, but DApps cannot integrate it by relying only on the EAS standard interface; they must still understand the resolver's custom rules.
- **Soulbound tokens ([ERC-5484](./eip-5484.md), [ERC-5192](./eip-5192.md), [ERC-4973](./eip-4973.md))**: SBTs use non-transferable [ERC-721](./eip-721.md) tokens to represent credentials. This ERC uses a registry-based approach with built-in named standard identifiers. Unlike persistent SBTs, this ERC intentionally limits active authorization to a single transaction.
- **[ERC-8004](./eip-8004.md) (Validation Registry)**: [ERC-8004](./eip-8004.md) includes both identity registration and validation registry mechanisms: the Identity Registry handles agent identity registration, while the Validation Registry handles per-task `validationRequest` / `validationResponse` flows and is asynchronous and advisory. This ERC does not replace [ERC-8004](./eip-8004.md) identity registration, nor does it deny its validation layer. It adds a synchronous, transaction-scoped, directly queryable active authorization primitive at the same conceptual validation layer, and should be positioned as a companion or extension to the Validation Registry. This ERC defines a normative [ERC-8004](./eip-8004.md) integration profile: any implementation that claims support for that profile must set `subjectId` equal to the [ERC-8004](./eip-8004.md) `agentId` and `subjectType` equal to `keccak256("ERC8004_AGENT")`. This requirement only applies to implementations claiming [ERC-8004](./eip-8004.md) integration support, and does not restrict other subject namespaces. Other subject types may still be defined by other profiles or by a future namespace registration mechanism.

### Attestor Operational Profile

The Attestor is on the synchronous hot path of the DApp call stack: every gated action requires the Attestor to evaluate and trigger `attestAndCall` within that transaction. If the Attestor is offline, it blocks all operations depending on that capability. This is a different model from the [ERC-8004](./eip-8004.md) asynchronous Validation Registry, which can tolerate validator unavailability, and [ERC-7715](./eip-7715.md)-style static policy distribution, which can be pre-programmed into wallets. If the action can be audited asynchronously, use [ERC-8004](./eip-8004.md). If the policy can be made static, use [ERC-7715](./eip-7715.md). Use this ERC when per-action off-chain evaluation must gate on-chain execution.

Attestor services **should** provide low latency, high availability, idempotent execution, where the same evaluation corresponds to the same `(capability, actionDigest)`, and auditable decisions anchored by `evidenceHash`.

### Deployment Model

This ERC is an implementable interface standard. It does not require or assume a per-chain singleton Registry. Any team may deploy an independent Registry, choose its own Attestors and capability set, similar to the "standard + multiple deployments" model of [ERC-721](./eip-721.md) / [ERC-1155](./eip-1155.md). This choice has an explicit cost: the same `capability` constant may not have the same meaning across different Registries. A DApp must not treat two Registries as equivalent authorization sources merely because they use the same `capability` name; it must also trust the specific Registry address, that Registry's Attestor set, and the corresponding authorization policy.

When integrating, a DApp **MUST** explicitly declare which Registry deployments it trusts, rather than relying only on [ERC-165](./eip-165.md) discovery. Different deployments may have entirely different Attestor sets and governance. Ecosystems that need cross-deployment composability should establish mutual recognition explicitly through a shared Registry, an upper-layer profile, governance agreement, or a future namespace registration mechanism.

### Upgradeability

Implementations **MAY** use proxy upgrades, such as UUPS, Beacon, or Transparent proxies, chosen by the implementation. During upgrades, the following invariants **should** be preserved:

- `supportsInterface` interface IDs remain stable;
- `Attested` event signature, including indexed topic order, remains stable;
- `attestationId` remains monotonic and continuous, and old IDs remain queryable through `getAttestation`;
- `AttestationRecord` ABI remains backward-compatible: no fields are removed, no semantics are changed, and new fields are appended at the end.

Upgrades may add interfaces or execution profiles, but must not reinterpret existing `capability`, `actionDigest`, historical records, or the transient semantics of transaction-scoped active authorization. Changes that break core protocol semantics, including mode selection, transient lifecycle, `msg.sender == wallet`, or `attestAndCall` as the single entry point, **SHOULD** be deployed at a new address rather than performed in-place.

## Backwards Compatibility

This ERC does not modify the behavior of any existing identity, token, or account standard. DApp contracts integrating this attestation mechanism discover the interfaces supported by the registry through [ERC-165](./eip-165.md). Standard implementations must support `IERC8273AtomicAttestation.attestAndCall` and represent transaction-scoped active authorization through transient storage.

This ERC depends on [EIP-1153](./eip-1153.md). When deployed to chains where `TSTORE` / `TLOAD` are not enabled, implementations must use an equivalent transaction-scoped authorization mechanism, or they must not claim full compatibility with this ERC.

**Any contract claiming to implement `IERC8273AtomicAttestation` MUST provide [EIP-1153](./eip-1153.md) or an equivalent transaction-scoped authorization clearing mechanism**. If it cannot provide such a mechanism, it must not claim support for the interface, because doing so would break the normative invariant that active authorization does not remain across transactions.

## Reference Implementation

The following implementation illustrates the transient-storage lifecycle and the shape of two execution profiles. Both profiles ship with a minimal runnable action-bound digest rule — `keccak256(abi.encode(calls, attestationId))` for Direct Wallet and `keccak256(abi.encode(wallet, handleOpsCalldata, attestationId))` for [ERC-4337](./eip-4337.md) — where `attestationId` is allocated by `_attestTransient` before dispatch and serves as a natural per-attestation uniqueness source. Production implementations MAY swap in stronger formulas (user-supplied nonce, session salts, etc.). The [ERC-4337](./eip-4337.md) profile additionally **MUST** decode the UserOperation to verify `sender == wallet` and prove target action success — the reference implementation does not decode calldata and only enforces the minimal digest binding. The Direct Wallet capability-only path (`actionDigest == 0`) runs out of the box and is convenient as an end-to-end entry point for testing the transient lifecycle.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract AttestationRegistry is
    IERC8273ActiveAttestation,
    IERC8273WalletAttestation,
    IERC8273AtomicAttestation
{
    bytes32 public constant AGENT_EXECUTE_V1 =
        keccak256("ERC8273_AGENT_EXECUTE_V1");
    bytes32 public constant ERC4337_USEROP_V1 =
        keccak256("ERC8273_ERC4337_USEROP_V1");

    mapping(uint256 => AttestationRecord) private _records;
    mapping(bytes32 => uint256) private _latestAttestations;
    uint256 private _nextId = 1;

    address public owner;
    mapping(address => bool) public authorizedAttestors;

    constructor() {
        owner = msg.sender;
        authorizedAttestors[msg.sender] = true;
    }

    modifier onlyAuthorizedAttestor() {
        require(authorizedAttestors[msg.sender], "not authorized attestor");
        _;
    }

    // each extension's id is XOR of ONLY its own declared selectors (matches `type(I).interfaceId`).
    bytes4 private constant _IERC8273_ID =
        IERC8273.isAttested.selector ^
        IERC8273.latestAttestationId.selector ^
        IERC8273.getAttestation.selector;
    bytes4 private constant _IERC8273_ACTIVE_ID =
        IERC8273ActiveAttestation.getActiveAttestation.selector;
    bytes4 private constant _IERC8273_WALLET_ID =
        IERC8273WalletAttestation.isAttestedAddress.selector ^
        IERC8273WalletAttestation.getActiveAttestationByWallet.selector;
    bytes4 private constant _IERC8273_ATOMIC_ID =
        IERC8273AtomicAttestation.attestAndCall.selector;

    function supportsInterface(bytes4 interfaceId)
        external pure override returns (bool)
    {
        return
            interfaceId == type(IERC165).interfaceId ||
            interfaceId == _IERC8273_ID ||
            interfaceId == _IERC8273_ACTIVE_ID ||
            interfaceId == _IERC8273_WALLET_ID ||
            interfaceId == _IERC8273_ATOMIC_ID;
    }

    function _subjectHash(uint256 subjectId, bytes32 subjectType)
        internal pure returns (bytes32)
    {
        return keccak256(abi.encode(subjectId, subjectType));
    }

    function _lookupKey(bytes32 sh, bytes32 capability, bytes32 actionDigest)
        internal pure returns (bytes32)
    {
        return keccak256(abi.encode(sh, capability, actionDigest));
    }

    function _walletTSlot(address wallet, bytes32 capability, bytes32 actionDigest)
        internal pure returns (bytes32)
    {
        return keccak256(abi.encode("wallet", wallet, capability, actionDigest));
    }

    function _subjectTSlot(bytes32 subjectHash, bytes32 capability, bytes32 actionDigest)
        internal pure returns (bytes32)
    {
        return keccak256(abi.encode("subject", subjectHash, capability, actionDigest));
    }

    function attestAndCall(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 evidenceHash,
        address wallet,
        ExecutionRequest calldata exec
    )
        external
        payable
        override
        onlyAuthorizedAttestor
        returns (uint256 attestationId, bytes memory result)
    {
        require(wallet != address(0), "zero wallet");
        require(capability != bytes32(0), "zero capability"); // prevent zero-capability attack vector
        require(exec.profileId != bytes32(0), "zero profile");
        // exec.actionDigest == 0 is allowed and means capability-only mode.
        // native value MUST go through action-bound mode.
        require(
            msg.value == 0 || exec.actionDigest != bytes32(0),
            "native value requires action-bound mode"
        );

        attestationId = _attestTransient(
            subject, capability, exec.actionDigest, evidenceHash, wallet
        );

        if (exec.profileId == AGENT_EXECUTE_V1) {
            result = _executeAgentProfile(wallet, exec, attestationId);
        } else if (exec.profileId == ERC4337_USEROP_V1) {
            result = _executeUserOpProfile(wallet, exec, attestationId);
        } else {
            revert("unsupported execution profile");
        }
    }

    function _attestTransient(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 actionDigest,
        bytes32 evidenceHash,
        address wallet
    ) internal returns (uint256 attestationId) {
        attestationId = _nextId++;
        bytes32 sh = _subjectHash(subject.subjectId, subject.subjectType);

        _records[attestationId] = AttestationRecord({
            subjectId: subject.subjectId,
            subjectType: subject.subjectType,
            attestor: msg.sender,
            capability: capability,
            actionDigest: actionDigest,
            issuedAt: uint64(block.timestamp),
            status: AttestationStatus.Recorded,
            evidenceHash: evidenceHash,
            wallet: wallet
        });

        _latestAttestations[_lookupKey(sh, capability, actionDigest)] = attestationId;

        bytes32 wSlot = _walletTSlot(wallet, capability, actionDigest);
        bytes32 sSlot = _subjectTSlot(sh, capability, actionDigest);
        assembly {
            tstore(wSlot, attestationId)
            tstore(sSlot, attestationId)
        }

        emit Attested(
            attestationId,
            wallet,
            capability,
            actionDigest,
            sh,
            msg.sender,
            subject.subjectId,
            subject.subjectType,
            evidenceHash
        );
    }

    // both profiles use `attestationId` as replay-safety source (allocated before dispatch). Production MAY swap in stronger rules.

    function _executeAgentProfile(
        address wallet,
        ExecutionRequest calldata exec,
        uint256 attestationId
    ) internal returns (bytes memory result) {
        (IAgentExecute.AgentCall[] memory calls, bytes memory authData) =
            abi.decode(exec.data, (IAgentExecute.AgentCall[], bytes));
        // Action-bound: minimal digest = keccak256(calls, attestationId). Capability-only skips the check.
        if (exec.actionDigest != bytes32(0)) {
            require(
                exec.actionDigest == keccak256(abi.encode(calls, attestationId)),
                "bad action digest"
            );
        }
        bytes[] memory results =
            IAgentExecute(wallet).executeFromRelayer{value: msg.value}(calls, authData);
        result = abi.encode(results);
    }

    function _executeUserOpProfile(
        address wallet,
        ExecutionRequest calldata exec,
        uint256 attestationId
    ) internal returns (bytes memory result) {
        // EntryPoint.handleOps is not payable; native prefund must use depositTo.
        require(msg.value == 0, "4337 profile rejects native value; use EntryPoint.depositTo");

        (address entryPoint, bytes memory handleOpsCalldata, ) =
            abi.decode(exec.data, (address, bytes, bytes));
        require(entryPoint != address(0), "zero entryPoint");

        // Minimal digest binds (wallet, handleOpsCalldata, attestationId).
        // Production adapters MUST also decode the UserOp, verify sender == wallet, and prove action success.
        if (exec.actionDigest != bytes32(0)) {
            require(
                exec.actionDigest ==
                    keccak256(abi.encode(wallet, handleOpsCalldata, attestationId)),
                "bad action digest"
            );
        }

        (bool ok, bytes memory ret) = entryPoint.call(handleOpsCalldata);
        if (!ok) {
            assembly {
                revert(add(ret, 32), mload(ret))
            }
        }
        result = ret;
    }

    function isAttested(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 actionDigest
    ) external view override returns (bool) {
        bytes32 sh = _subjectHash(subject.subjectId, subject.subjectType);
        bytes32 slot = _subjectTSlot(sh, capability, actionDigest);
        uint256 id;
        assembly { id := tload(slot) }
        return id != 0;
    }

    function latestAttestationId(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 actionDigest
    ) external view override returns (uint256) {
        return _latestAttestations[_lookupKey(
            _subjectHash(subject.subjectId, subject.subjectType),
            capability,
            actionDigest
        )];
    }

    function getAttestation(uint256 attestationId)
        external view override returns (AttestationRecord memory record)
    {
        record = _records[attestationId];
    }

    function getActiveAttestation(
        SubjectRef calldata subject,
        bytes32 capability,
        bytes32 actionDigest
    ) external view override returns (AttestationRecord memory record) {
        bytes32 sh = _subjectHash(subject.subjectId, subject.subjectType);
        bytes32 slot = _subjectTSlot(sh, capability, actionDigest);
        uint256 id;
        assembly { id := tload(slot) }
        require(id != 0, "no active attestation");
        record = _records[id];
    }

    function isAttestedAddress(
        address wallet,
        bytes32 capability,
        bytes32 actionDigest
    ) external view override returns (bool) {
        bytes32 slot = _walletTSlot(wallet, capability, actionDigest);
        uint256 id;
        assembly { id := tload(slot) }
        return id != 0;
    }

    function getActiveAttestationByWallet(
        address wallet,
        bytes32 capability,
        bytes32 actionDigest
    ) external view override returns (AttestationRecord memory record) {
        bytes32 slot = _walletTSlot(wallet, capability, actionDigest);
        uint256 id;
        assembly { id := tload(slot) }
        require(id != 0, "no active attestation");
        record = _records[id];
    }
}
```

## Security Considerations

### Transient Storage Authorization Model

This ERC stores active authorization entirely in transient storage. Transient storage is automatically cleared by the EVM at the end of each transaction, structurally preventing an attestation from surviving beyond the transaction in which it is issued. The risk of active authorization remaining after transaction end is eliminated at the protocol layer rather than relying on the Attestor or operational discipline.

If an execution profile reverts for any reason, the entire transaction reverts, including `TSTORE` writes and persistent audit records. The system cannot enter a state where the action failed but authorization remains.

### Execution Profile Security

Each execution profile must clearly define:

- the encoding of `exec.data`;
- how `exec.actionDigest` is derived from the authorized action, only required in action-bound mode; in capability-only mode `actionDigest = 0`, and the profile does not perform digest validation;
- how the target call's `msg.sender` is guaranteed to be `wallet`, with the concrete mechanism defined by each profile; see the Execution Profiles section above;
- how successful execution of the target action is confirmed;
- how `msg.value` is handled, including whether it is forwarded, whether non-zero value is rejected, whether the wallet may use an existing balance, and how native value is included in `exec.actionDigest`;
- who is responsible for replay protection and domain separation.

**Additional note on capability-only mode**: when `exec.actionDigest = 0`, the profile does not validate matching between calls and digest. The attestation authorizes the wallet to perform "any" action accepted by that profile under the capability. Before issuing a capability-only attestation, the Attestor **must** confirm in its off-chain evaluation that the submitted calls fall within the policy boundary of the capability. Profile implementations should document this clearly, so capability-only mode is not misunderstood as "calls do not need safety review."

The direct wallet profile must trust the wallet to correctly verify `authData`. If `executeFromRelayer` is designed without `msg.sender` restrictions, then `authData` must bind chainId, wallet, registry, profile, actionDigest, nonce, and validity period. If a caller with valid `authData` bypasses the Registry and calls the wallet directly, the target DApp's attestation gate will revert because no transient slot was written; however, this assumes that the DApp actually performs `getActiveAttestationByWallet` gating.

The [ERC-4337](./eip-4337.md) UserOperation profile must confirm that the UserOperation's `sender == wallet` and that the UserOperation is authorized by the agentAA's own nonce, signature, or module policy. The agent wallet **SHOULD NOT** grant the Attestor reusable execution authority (long-lived session keys, expiry-less module authorizations). This ERC cannot enforce that at the protocol layer, but ignoring it widens a compromised Attestor's blast radius from "one evaluation" to "the agent's entire assets". The Attestor **should** submit only the UserOp approved by the current evaluation.

Implementations must not infer successful target action execution solely because the low-level call to `EntryPoint.handleOps` did not revert. If the EntryPoint or account implementation may record UserOperation execution failure as an event rather than bubbling a revert, the profile adapter must confirm success through an account receipt, DApp receipt, or another verifiable postcondition; otherwise it must revert.

### Choosing the Correct Gating Primitive

Contracts gating sensitive on-chain operations **must** use `getActiveAttestation(subject, capability, actionDigest)` or `getActiveAttestationByWallet(wallet, capability, actionDigest)`, which revert when the transient slot is empty. They **must not** use `isAttested` / `isAttestedAddress`, which are bool-returning view helpers.

Bool-returning `view` functions make it easy for integrators to write incorrect conditional branches or forget the check. The reverting variants cause the whole transaction to revert when an attestation is absent, structurally eliminating this class of error. The following pattern is not recommended:

```solidity
// Bad example: sensitive on-chain gating must not rely only on a bool snapshot.
require(
    registry.isAttestedAddress(msg.sender, capability, actionDigest),
    "no attestation"
);
_doSensitive();
```

The recommended pattern is:

```solidity
IERC8273.AttestationRecord memory record =
    registry.getActiveAttestationByWallet(msg.sender, capability, actionDigest);
_doSensitive();
```

| Function | Semantics | Recommended Use |
| --- | --- | --- |
| `getActiveAttestation(..., capability, actionDigest)` / `getActiveAttestationByWallet(..., capability, actionDigest)` | Reads transient storage; reverts if the slot is empty | **On-chain authorization gating (recommended)** |
| `isAttested(..., capability, actionDigest)` / `isAttestedAddress(..., capability, actionDigest)` | Reads transient storage; returns bool | Off-chain indexers and UX display; must not be the sole gate for sensitive on-chain operations |

### `actionDigest` Derivation

The `actionDigest` derivation rule is negotiated by the DApp integrating this attestation mechanism and the Attestor, but it must satisfy the following constraints:

- When `actionDigest != 0`, it **must** bind the target contract, function selector, arguments, and a nonce or other uniqueness source, such as an attestationId or user-supplied salt. Without a nonce, two identical actions have the same `actionDigest`, which theoretically leaves room for replay.
- When the action carries native tokens, the amount **must** be included in `actionDigest`.
- The DApp must recompute `expectedActionDigest` using the same rule at the gating point and query with that value.
- `chainid` need not be included in `actionDigest`, because per-chain registry deployment already provides isolation. `exec.profileId` need not be included in `actionDigest`, because the execution profile is an internal Registry concept. Only DApps whose security model truly needs to distinguish call paths should explicitly include it.
- A mismatch between Attestor and DApp derivation rules is a common integration error: the Attestor attests under `(capability, digestA)` while the DApp queries `(capability, digestB)`, causing the gating query to revert. This is not a vulnerability, but it is a deployment error and must be covered by tests.

Capability-only mode (`actionDigest == 0`) means that "for this wallet and this capability, authorization covers any action included under that capability." It should only be used when the gated action itself is a coarse-grained capability check, such as "is this an authenticated agent." High-risk actions should use action-bound mode.

### Reentrancy

`getActiveAttestation`* reads from transient storage at call time and **does not** lock state for the remainder of the transaction. **Important**: in capability-only mode (`actionDigest == 0`), the transient slot remains active during the issuing transaction, meaning the attestation gate itself **does not prevent** reentrant calls within the same transaction. If an attacker can reenter the gated function during the action execution stack, the second call still passes the gate. This differs from the intuition of "single-use." In action-bound mode, because `actionDigest` usually includes a nonce, the DApp can prevent reentry by invalidating the nonce after execution, but this is the DApp's responsibility, not a guarantee provided by the Registry. In all modes, contracts gating sensitive operations **must** use a reentrancy guard around the gated operation.

### Registry Trust

DApp contracts integrating this attestation mechanism trust the registry's Attestor authorization policy. Weak governance may issue unsafe attestations. Implementations **should** provide a bounded authorized Attestor set and robust processes for adding and removing Attestors. Specific risks include: compromise of a single Attestor can cause arbitrary actions within its authority to be attested; governance multisig latency can increase damage during the compromise window.

### Attestor Compromise

A compromised Attestor can issue attestations for arbitrary subjects within its authority. Each attestation is active only within its issuing transaction, so compromise does not leave persistent unauthorized active state. However, during the compromise window, the compromised party can initiate **any number** of transactions, each with an attestation. "Blast radius limited to one transaction" refers to the blast radius of a single attestation, not the total damage from the compromise. Total damage depends on how quickly the compromise is detected and the Attestor's authority is revoked. Implementations should use the capability namespace to constrain Attestor authority; operators should monitor `Attested` events and their corresponding execution profile calls.

### Subject Control Change

If the effective controller of a subject changes, historical attestations may no longer be valid. High-risk scenarios **should** require fresh attestation rather than relying on previously issued attestations. Because all active authorization is transient, there is no persistent authorization that needs to be invalidated; this concern primarily applies to audit records in `_records`.

### Evidence Integrity

Off-chain evidence must remain consistent with `evidenceHash`. Immutable references such as IPFS are recommended. Mutable storage, such as an HTTP URL without content addressing, **must not** be the sole backing reference for `evidenceHash`.

### Cross-Chain Limits

`SubjectRef` does not include `chainId`, and each registry is deployed per chain. A subject reference on one chain must not be assumed to have meaning on another chain. Cross-chain DApp contracts **should** re-attest on each chain rather than relying on bridged attestations.

**`attestationId` is not globally unique**: `_nextId` is monotonic only within a single Registry on a single chain. IDs may collide across Registries or chains. Indexers, bridges, and audit tools **MUST use the `(chainId, registry, attestationId)` tuple**. The `Attested` event itself does not include the first two fields; the indexing layer must inject them.

### Wallet Binding Scope

`isAttestedAddress(wallet, capability, actionDigest)` and `getActiveAttestationByWallet(wallet, capability, actionDigest)` are scoped by the `(wallet, capability, actionDigest)` tuple and by a single transaction. Transient storage slots for different tuples are independent and are all automatically cleared at the end of the transaction.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
