EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8356: Purpose-Bound Third-Party Data Consent

A revocable, purpose-bound consent grant in which the consenting subject is not the beneficiary, with an independently revocable agent leg.

Authors
Created2026-07-30
Discussion Linkhttps://ethereum-magicians.org/t/erc-8356-purpose-bound-third-party-data-consent/29217
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1921PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationDefinitionsData modelInterfaceRequired behaviourContent Credentials profileRationalePrior ArtBackwards CompatibilitySecurity ConsiderationsPrivacy and Data ProtectionCopyright

Abstract

This standard defines a consent grant in which three principals are distinguished: a subject who consents, a grantee who receives access, and an optional agent that acts on the grantee's behalf. The subject is not the beneficiary. The grant is bound to a declared purpose drawn from a versioned code registry, carries a validity window and a usage cap, may be attenuated into sub-grants that can never exceed their parent, and can be revoked either in whole by the subject or in part by terminating only the agent leg. Grants are minted against an existing asset token so that consent appears in that asset's provenance.

The on-chain record is a status anchor, not the instrument. The instrument, the human-readable authorization the subject actually assented to, remains off-chain and is referenced by hash. What the chain contributes is public, non-repudiation, consensus-ordered revocation, and the ability to prove that a grant existed before a given access.

Motivation

Delegated access to regulated data is now routinely exercised by software agents acting for a human principal. Existing standards cannot express this safely, for three reasons.

A datum about a person is rarely a single object. It is a chain of derived artifacts, each produced by a different custodian and each further from the person than the last. A biological sample is the clearest case:

flowchart LR
  subgraph Person["The person"]
    Sample["Biosample"]
  end
  subgraph Lab["Sequencing laboratory"]
    FASTQ["FASTQ"]
    VCF["VCF"]
  end
  subgraph Annot["Annotation service"]
    AVCF["Annotated VCF"]
    Store["Queryable record"]
  end
  subgraph Model["Model provider"]
    Extract["Training extract"]
    Vectors["Embeddings"]
    Weights["Model weights"]
  end
  Sample -- "sequencing" --> FASTQ
  FASTQ -- "variant calling" --> VCF
  VCF -- "annotation" --> AVCF
  AVCF -- "indexing" --> Store
  Store -- "extraction" --> Extract
  Extract -- "embedding" --> Vectors
  Vectors -- "training" --> Weights

One consent decision at the left governs seven transformations across four custodians, and the person is asked again at none of them. Two properties follow, and no existing standard provides either. The permission must compose along the chain, narrowing at each hop and never widening. And withdrawal must run the chain in reverse, which it can do until it reaches model weights, where deletion is not available and only measurement and accounting remain.

Every existing delegation standard is two-party. In ERC-4907 and ERC-5006 a token owner grants a time-boxed user role over their own token. In ERC-8226 a principal delegates scoped, capped authority to an agent over the principal's own asset, with the mandate keyed (agent, principal). In every case the party who consents is the party who benefits, and the party at risk is the party who signed. The case this standard addresses is structurally different: a subject consents to a grantee's access to data about the subject, and the harm from an unauthorized act falls on someone who is not a party to the transaction at all.

Other standards do model a subject. ERC-8328 defines subjectId, ROLE_SUBJECT and ROLE_BENEFICIARY; W3C VC 2.0 separates credentialSubject from holder and issuer. What neither does is make that non-party subject's withdrawal operative for a relying party at access time. ERC-8328 says so itself: it "does not define compliance policy... transfer restrictions", and its OUTCOME_REVOKED is a recorded assertion that nothing consults. That is the gap this specification closes, and it is the whole of the claim.

The agent cannot be revoked separately from the human. Where an agent is modeled, it is modeled as an extension of the principal. There is no way to say "this researcher retains access, but the tool they were using does not," which is precisely the action an incident response requires when a model provider, an MCP server, or a session key is compromised.

Purpose is not enforced anywhere. Regulated-data authorizations are purpose-limited by construction, and no EIP enforces purpose at all. Every regulatory-compliance ERC to date (ERC-3643, ERC-7518, ERC-8106, ERC-8226, ERC-8320) encodes securities and real-world-asset vocabulary: accreditation, jurisdiction blocks, AML flags, transfer caps. A fund unit has no purpose limitation, so the vocabulary was never needed. Data has one.

There is also a concrete gap in the incumbent non-blockchain practice this standard is meant to interoperate with rather than replace. The GA4GH Authentication and Authorization Infrastructure profile, the incumbent mechanism for credentialed access to controlled genomic datasets, states that a Visa Issuer "MAY provide tokens of this type without any revocation process"; it defines exp as explicitly not a hard cutoff, noting that access "is NOT necessarily removed by the exp timestamp"; and it rate-limits the only active validity check, requiring that polling "MUST NOT be done more than once per hour per Passport Clearinghouse." There is no revocation list and no push mechanism. Worst-case propagation of a withdrawn consent is therefore bounded only by token lifetime and an hourly poll.

A withdrawal that takes an hour to propagate is a poor fit for legal regimes whose operative triggers are knowledge and ease. This standard's contribution is to make the revocation itself public, timestamped, non-repudiation, and immediately readable by any relying party, without moving the authorization document, the identity, or the data on-chain.

Specification

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

Definitions

  • Subject: the natural or legal person whose data the grant concerns, and whose assent the grant records. The subject is not required to be the owner of the asset token.
  • Grantee: the party authorized to exercise the grant.
  • Agent: an optional principal that exercises the grant on the grantee's behalf.
  • Instrument: the off-chain authorization document the subject assented to, referenced on-chain by termsRef and never stored on-chain.
  • Agent operator: the party accountable for the agent, typically the organization running the software the agent is. Named separately from the agent because containing a compromised agent must not require the agent itself to cooperate.

Data model

enum Status {
    NONE,                  // 0  unset
    ACTIVE,                // 1
    REVOKED_BY_SUBJECT,    // 2  withdrawal by the subject
    RENOUNCED_BY_GRANTEE,  // 3  handed back by the grantee
    TERMINATED_BY_ISSUER,  // 4  programme or study closed
    SUSPENDED,             // 5  temporary hold; resume is the only non-terminal return to ACTIVE
    EXHAUSTED,             // 6  usesMax reached
    SUPERSEDED             // 7  replaced by a later grant
}

enum AgentKind {
    NONE,                  // 0  no agent leg
    SESSION_KEY,           // 1  ephemeral key, proves control by signature
    EXTERNAL_REGISTRY,     // 2  reference into an agent identity registry (e.g. ERC-8004)
    TOKEN_BOUND_ACCOUNT,   // 3  ERC-6551 account
    ASSERTED_UNVERIFIED    // 4  claimed by the grantee, NOT proven
}

A conforming implementation MUST store, for each grant:

struct ConsentGrant {
    // slot 0
    bytes32 scopeRef;           // resource identifier hash, or Merkle root if FLAG_SCOPE_IS_ROOT
    // slot 1
    bytes32 purposeProfileRef;  // hash of the canonical purpose profile document
    // slot 2
    bytes32 termsRef;           // hash of the VERSIONED TEMPLATE instrument, never a per-subject copy
    // slot 3
    bytes32 parentGrantId;      // 0 for a root grant
    // slot 4
    uint256 assetTokenId;       // the asset this grant is recorded against
    // slot 5
    bytes32 subjectCommit;      // commitment to a per-grant subject key; never an address
    // slot 6
    address grantee;
    uint48  notBefore;
    uint48  expiresAt;
    // slot 7
    address agent;
    uint32  ops;                // permission bitmask
    uint32  purposeCode;        // index into the purpose registry
    uint8   status;             // Status
    uint8   flags;
    uint16  depth;              // delegation depth, 0 for a root grant
    // slot 8
    uint8   agentKind;          // AgentKind
    uint8   agentStatus;        // revocable independently of `status`
    uint48  lastUsedAt;
    uint16  useCount;
    uint16  usesMax;            // 0 means unlimited
}

Field order is RECOMMENDED rather than required; the layout above occupies eight storage slots, and reordering costs gas on every grant without changing behaviour.

Calldata encoding is an implementation choice, and a constrained one. The storage layout above is normative; how a grant is passed into grant and subGrant is not. Implementers should be aware that a struct of this width in an external function signature generates an ABI decoder that exceeds the EVM stack, and does not compile, even with the IR pipeline enabled. The reference implementation therefore accepts a narrower input struct in which the seven small scalars (notBefore, expiresAt, ops, purposeCode, usesMax, agentKind, flags) are packed into a single word, with encodePolicy and decodePolicy helpers giving named access. Any encoding that yields the normative storage layout conforms. An input struct MUST NOT expose the contract-controlled fields (status, depth, parentGrantId, agentStatus, useCount, lastUsedAt): a caller able to set its own status could forge an active grant, and one able to set depth could escape the delegation bound.

flags bits: FLAG_SCOPE_IS_ROOT = 0x01, FLAG_DELEGABLE = 0x02, FLAG_DESTROY_DERIVATIVES = 0x04. Bits 0x08 through 0x80 are reserved and MUST be zero.

ops bits, normative. Without fixed values, two parties assign different meanings to the same bit position: a grant minted to permit evaluation is read by the data holder as permitting export, and the failure direction is disclosure:

bitnamemeaning
0x01READretrieve the resource
0x02QUERYevaluate over it without retrieving it
0x04DERIVEproduce a new artifact from it
0x08EXPORTmove it outside the enforcing boundary
0x10TRAINuse it to fit model parameters

Bits 0x20 and above are reserved. An unrecognized bit MUST NOT be treated as granting anything. Implementations MAY additionally expose these as ERC-6617 permissions; the values are fixed here because a grant minted by one party is evaluated by another, and indices assigned independently collide.

Interface

interface IConsentGrant /* is IERC165 */ {

    event ConsentGranted(
        bytes32 indexed grantId, uint256 indexed assetTokenId,
        address indexed grantee, uint32 purposeCode, uint48 expiresAt);
    event ConsentSubGranted(bytes32 indexed grantId, bytes32 indexed parentGrantId, uint16 depth);
    event ConsentRevoked(bytes32 indexed grantId, bytes32 indexed by, uint8 status, bytes32 reasonCode);
    event AgentRevoked(bytes32 indexed grantId, address indexed agent, bytes32 reasonCode);
    event ScopeLeafRevoked(bytes32 indexed grantId, bytes32 leaf);
    event UsageCommitted(bytes32 indexed grantId, uint64 epoch, bytes32 headHash, uint32 count);

    function grant(ConsentGrant calldata g, bytes calldata subjectSig)
        external returns (bytes32 grantId);

    function subGrant(bytes32 parentGrantId, ConsentGrant calldata g, bytes32[] calldata scopeProof)
        external returns (bytes32 grantId);

    function revokeBySubject(bytes32 grantId, bytes32 reasonCode) external;
    // Authenticates the caller against `subjectCommit`. A subject holding no account
    // uses `revokeBySubjectWithSig`, which requires neither a wallet nor gas.
    function renounceByGrantee(bytes32 grantId) external;
    function revokeAgent(bytes32 grantId, bytes32 reasonCode) external;
    function revokeScopeLeaf(bytes32 grantId, bytes32 leaf) external;

    // Relayed equivalents. A subject holding no fee token MUST still be able to withdraw,
    // so every subject-authored act has a signed path a third party can submit.
    function revokeBySubjectWithSig(bytes32 grantId, bytes32 reasonCode, bytes calldata subjectSig)
        external;
    function revokeScopeLeafWithSig(bytes32 grantId, bytes32 leaf, bytes calldata subjectSig)
        external;

    /// Incident-response hold. Only the grantee or agent operator. See Status transitions.
    function suspend(bytes32 grantId, bytes32 reasonCode) external;
    /// Clears SUSPENDED back to ACTIVE. Only the party that suspended, never an operator undoing
    /// a subject withdrawal.
    function resume(bytes32 grantId) external;
    /// Programme or study closure. Only an issuer role defined by the implementation.
    function terminateByIssuer(bytes32 grantId, bytes32 reasonCode) external;

    function checkAccess(
        bytes32 grantId, uint32 purposeCode, uint32 op,
        bytes32 resourceHash, bytes32[] calldata proof
    ) external view returns (bool);

    function isGrantActive(bytes32 grantId) external view returns (bool);

    /// The status to DISPLAY: this grant's own, or the first terminal status inherited
    /// from an ancestor. Companion to lazy cascade; `isGrantActive` remains the
    /// authorization answer.
    function effectiveStatus(bytes32 grantId) external view returns (uint8);

    /// Monotonic per-subject signature nonce. Without it a relayed grant or revocation is
    /// replayable against the same contract.
    function subjectOf(bytes32 grantId) external view returns (bytes32 subjectCommit);

    function nonces(bytes32 subjectCommit) external view returns (uint256);

    function revocationEpoch() external view returns (uint256);

    function commitUsage(bytes32 grantId, uint64 epoch, bytes32 headHash, uint32 count) external;
}

Required behaviour

Subject commitment. subjectCommit is the only on-chain reference to the subject. It MUST be formed as:

subjectCommit = keccak256(abi.encode(subjectSalt, subjectKey))

where subjectSalt is drawn from at least 128 bits of entropy, is unique to the grant, and is never written on-chain, and subjectKey is the address whose signatures authorize every subject-authored act for that grant (an EOA recovered from the signature, or an ERC-1271 contract that validated it). A bare keccak256(subjectKey) without a salt MUST NOT be used: an address space is enumerable, and Privacy rule 2 forbids unsalted hashes over enumerable spaces. A fresh subjectKey SHOULD be generated per grant so that a long-lived wallet address is not the opening.

Authorship of consent. grant MUST require either (a) msg.sender == subjectKey for an opening of g.subjectCommit, or (b) a valid EIP-712 signature by subjectKey, verified with ERC-1271 support for contract accounts, over the normative typed data below. An implementation MUST NOT permit an operator, issuer, or minter to create a grant on a subject's behalf without such authorship. Call paths that accept subjectSalt as a parameter MUST treat that parameter as authentication material, not as chain state.

The signed payload is normative, because a signature that is not portable across implementations defeats the purpose of naming EIP-712 at all. The domain is name: "ConsentGrant", version: "1", with the chain id and the verifying contract address. The struct is:

Grant(bytes32 scopeRef,bytes32 purposeProfileRef,bytes32 termsRef,uint256 assetTokenId,
      bytes32 subjectCommit,address grantee,address agent,uint256 policy,uint256 nonce)

policy packs the seven small scalars, and its layout is normative here even though the calldata encoding is not. A signed payload that referenced an implementation-defined encoding would produce a different digest per implementation, which is the portability failure this paragraph exists to prevent:

bitsfieldtype
0 to 47notBeforeuint48
48 to 95expiresAtuint48
96 to 127opsuint32
128 to 159purposeCodeuint32
160 to 175usesMaxuint16
176 to 183agentKinduint8
184 to 191flagsuint8

Bits 192 and above are reserved and MUST be zero. A known limitation: a wallet cannot render seven packed fields to the person being asked to consent, which is the harm EIP-712 exists to prevent. An implementation MAY instead sign the seven scalars as named fields, and a future revision of this specification may require it.

Replay. Domain separation alone stops cross-chain and cross-contract replay and does nothing about replay against the same contract. Without a nonce a relayer holding one subjectSig can call grant again after the subject has revoked, minting a fresh identical ACTIVE grant. That is reinstatement by another name, and it defeats two MUSTs in this document. Implementations therefore MUST maintain a monotonic per-subject nonce keyed by subjectCommit, MUST expose it as nonces(bytes32), and MUST consume it whenever a signature is accepted. The same applies to every signed revocation path.

Grant identifiers. grant MUST revert if the identifier already exists. Without that rule an implementation whose derivation collides silently overwrites a live grant's status, which is a back door around no-reinstatement. This specification does not mandate a particular preimage.

Relayed withdrawal. An implementation MUST accept a subject-signed revocation submitted by a third party (revokeBySubjectWithSig, revokeScopeLeafWithSig), so that a subject holding no fee token can withdraw. A design where granting accepts a relayed signature and withdrawal does not is non-conforming, because it makes withdrawal strictly harder than granting.

Attenuation. subGrant MUST be called by the parent's grantee, and MUST revert unless the parent has FLAG_DELEGABLE set and all of the following hold: child subjectCommit equals parent subjectCommit; child ops is a subset of parent ops; child expiresAt is not later than the parent's; child notBefore is not earlier than the parent's; child usesMax does not exceed the parent's (where 0 means unlimited); child purposeCode is the parent's or a registered narrowing of it; and depth does not exceed the implementation's maxDepth.

Scope containment MUST be proven, not assumed. Subtree membership is not decidable from two 32-byte roots, so an implementation cannot satisfy "child scope is within parent scope" by inspection. Either the child names the same scopeRef as the parent, or the parent is root-scoped, the child is leaf-scoped, and scopeProof proves the child's scopeRef is a leaf under the parent's root. An implementation MUST reject a child whose membership is neither of those. Accepting an unproven child scope voids attenuation entirely for Merkle-scoped grants: the grantee can mint a child naming any resource at all, and a later checkAccess on that child consults only the child's own scopeRef.

Agent verification. checkAccess MUST verify that msg.sender is the agent, or the grantee when agentKind == NONE. An implementation MUST NOT accept a caller-supplied agent identity as satisfying an agent constraint. agentKind == ASSERTED_UNVERIFIED MUST NOT satisfy an agent constraint; it is recorded for audit only.

checkAccess authenticates only an on-chain caller. The function reads msg.sender. An off-chain relying party that invokes it through eth_call (or any eth_call equivalent) supplies from itself and obtains no authentication whatsoever: a client can set from to the agent address and receive true for a grant that agent was never authorized to exercise from that client. For off-chain enforcement, a relying party MUST NOT treat a lone checkAccess eth_call as proof of caller identity. It MUST combine isGrantActive / effectiveStatus (which do not depend on msg.sender) with an independent authentication of the agent or grantee (for example a signature over the access request, an ERC-8004 agent session, or a transport-level credential). On-chain callers that gate a resource behind checkAccess in the same transaction remain sound, because msg.sender is then the EVM caller.

Scope proof. When FLAG_SCOPE_IS_ROOT is set, checkAccess MUST verify proof as a Merkle inclusion proof of resourceHash under scopeRef, and MUST return false if resourceHash has been withdrawn via revokeScopeLeaf on this grant or any ancestor. When the flag is clear, scopeRef MUST equal resourceHash.

Merkle construction is normative. Without a fixed leaf and pair rule, two conforming implementations can disagree on whether a proof is valid. Implementations MUST use:

  1. Leaf. leaf = keccak256(bytes.concat(keccak256(resourceId))), where resourceId is the canonical byte encoding of the resource identifier agreed off-chain for that asset. Double-hashing matches the OpenZeppelin MerkleProof / StandardMerkleTree leaf form and blocks second-preimage attacks on interior nodes presented as leaves.
  2. Interior node. parent = keccak256(bytes.concat(min(a, b), max(a, b))) over the two 32-byte children, i.e. sorted-pair hashing. Proof elements are sibling hashes from leaf to root; at each level the implementation sorts the current hash with the sibling before hashing.
  3. Empty proof. A single-leaf tree has scopeRef == leaf and an empty proof array.

revokeScopeLeaf withdrawals MUST be consulted on every ancestor when evaluating a descendant. A per-datum withdrawal that stops at the grant that recorded it does not survive one delegation hop, and would let a grantee recover a withdrawn leaf by minting a child that re-lists it.

Purpose registry. purposeCode indexes a registry that maps a code to an ontology term and pins the ontology release, so a code's meaning cannot drift. An implementation MUST expose purposeRegistry() returning its registry address, and the registry MUST implement:

interface IPurposeRegistry {
    function isNarrowing(uint32 childCode, uint32 parentCode) external view returns (bool);
}

Where purposeRegistry() returns the zero address, subGrant MUST require exact purpose equality; that is, an unset registry MUST fail closed. On-chain purpose matching is equality over registered codes. Ontology subsumption reasoning, where a broad permitted use implies a narrower one, is explicitly OUT OF SCOPE and is performed off-chain by the relying party.

Revocation authority.

ActionAuthorized callerResulting status
revokeBySubjectthe subject onlyREVOKED_BY_SUBJECT
renounceByGranteethe granteeRENOUNCED_BY_GRANTEE
revokeAgentgrantee, agent, or agent operatoragentStatus only; grant survives
revokeScopeLeafthe subjectgrant survives, leaf withdrawn

revokeBySubject MUST be unconditional, MUST take effect immediately, MUST NOT be blocked by the grantee, the issuer, or the contract operator, and MUST cause isGrantActive to return false for every descendant grant. It MUST NOT require unbounded work in the revoking transaction; lazy evaluation is permitted and RECOMMENDED. An eager per-descendant write lets a grantee mint enough sub-grants to push the subject's withdrawal past the block gas limit, which makes the withdrawal permanently impossible to execute and defeats the "MUST NOT be blocked" rule in the same sentence that states it.

Status transitions. Every non-NONE value MUST have exactly one writing path. Values with no path are dead weight and invite non-conforming custom setters.

FromToWriterNotes
NONEACTIVEgrant / subGrantonly mint path
ACTIVEREVOKED_BY_SUBJECTrevokeBySubject / WithSigterminal
ACTIVERENOUNCED_BY_GRANTEErenounceByGranteeterminal
ACTIVETERMINATED_BY_ISSUERterminateByIssuerterminal; issuer role is implementation-defined
ACTIVESUSPENDEDsuspendgrantee or agent operator only
SUSPENDEDACTIVEresumeonly the party that called suspend
SUSPENDEDREVOKED_BY_SUBJECTrevokeBySubject / WithSigsubject always wins
ACTIVE or SUSPENDEDEXHAUSTEDcommitUsagewhen cumulative uses reach usesMax != 0
ACTIVESUPERSEDEDimplementation supersede pathMUST mint the replacement in the same transaction

REVOKED_BY_SUBJECT, RENOUNCED_BY_GRANTEE, TERMINATED_BY_ISSUER, EXHAUSTED, and SUPERSEDED are terminal. An implementation MUST NOT provide any function that returns a terminal grant to ACTIVE. Re-granting MUST mint a new grant with a new identifier. The sole exception to no-reinstatement is resume of SUSPENDED → ACTIVE, and it exists only so incident response can lift a temporary hold without erasing a later subject withdrawal. resume MUST revert if the caller is not the party that suspended, and MUST revert if status is anything other than SUSPENDED.

Interface detection. A conforming implementation MUST return true from ERC-165 supportsInterface for the identifier of IConsentGrant. grant and subGrant are deliberately excluded from that interface: their selectors depend on the calldata encoding of the input struct, which this specification leaves to the implementation, so including them would make the identifier vary between conforming implementations.

Activity. isGrantActive MUST evaluate the grant and every ancestor up to maxDepth, and MUST return false if any is not ACTIVE, if the current time is outside [notBefore, expiresAt), or if usesMax != 0 && useCount >= usesMax.

Revocation epoch. revocationEpoch MUST increase on every revocation of any kind. Relying parties that cache authorization decisions SHOULD re-validate when it changes; this bounds revocation latency without requiring per-access reads.

Usage accounting. Implementations MUST NOT emit a per-access event. Usage is recorded off-chain as an append-only hash chain whose head is committed periodically via commitUsage. The chain is normative so that two parties can recompute the same head:

  1. Hash function. H is keccak256. h_0 = bytes32(0).
  2. Step. h_i = keccak256(abi.encodePacked(h_{i-1}, record_i)) for i = 1..n.
  3. Record. Each record_i is 32 bytes: record_i = keccak256(abi.encode( bytes32 grantId, uint64 accessedAt, uint32 op, bytes32 resourceHash, bytes32 requestId)). accessedAt is Unix seconds; requestId is a caller-chosen unique id for the access so that two identical accesses do not collide into one record. Fields finer than needed for enforcement (identity of a human user, session tokens) MUST NOT appear in record_i.
  4. Commitment. commitUsage(grantId, epoch, headHash, count) asserts that after count records in epoch, the chain head is headHash. count MUST be at least the previously committed count for that grant and epoch (monotonic). When usesMax != 0 and count >= usesMax, status MUST become EXHAUSTED and MUST NOT overwrite a terminal subject or issuer status.

usesMax is not a cryptographic bound. Only the grantee or agent can advance the off-chain chain and call commitUsage, so the party constrained by usesMax is also the party that reports use. The field is a contractual ceiling with on-chain accounting the constrained party self-declares. A relying party that needs a bound the grantee cannot under-report MUST meter access outside this mechanism (for example by serving bytes only through a gate that increments its own counter, or by requiring a third-party notary to call commitUsage). Implementations MUST document this honesty assumption wherever they surface usesMax to a subject.

What the chain provides is tamper-evidence and ordering over the records that were appended. It does not provide completeness: a party that declines to append a record leaves no gap a third party can detect from the head alone. See Privacy Considerations.

Content Credentials profile

This subsection is OPTIONAL. It is not a separate standard. A deployment MAY bind a grant, or a subject-less authorship claim, to a C2PA Content Credential so that a viewer can establish who committed to a piece of content and, when the content carries someone's data, whether that person's consent still stands. A conforming core implementation need not implement it, and an implementation that does implement it remains a conforming ERC-8356 implementation.

Two graphs stay two graphs. A C2PA manifest already carries an ingredient graph: the assets a derivative was made from. This ERC carries a consent graph: parentGrantId attenuation and scopeRef leaves. The profile MUST NOT collapse one into the other. Revoking a parent asset orphans its derivatives through the C2PA ingredient chain and FLAG_DESTROY_DERIVATIVES; revoking a grant does not rewrite any C2PA manifest. Withdrawal of consent is not erasure of authorship, and the two graphs answer different questions.

Content commitment. The content is committed as keccak256(abi.encodePacked(salt, sha256(content))), where salt is a fresh 32-byte value and sha256(content) is the SHA-256 of the exact bytes. abi.encodePacked of two bytes32 values is their concatenation, so this is keccak256(salt || sha256(content)). The salt is off-chain content, not on-chain state; it MUST carry at least 128 bits of entropy (Privacy rule 7) and MUST NOT be published on chain. A manifest commitment over the finalized C2PA bytes is formed the same way, keccak256(salt || sha256(manifest)), with an independent salt.

Binding ceremony. The order is normative. Bind is an on-chain attestation of the sealed C2PA claim, not a field inside it. Putting Bind inside the hashed claim would make manifestCommitment unknowable at signing time and would let a relayer swap the sealed bytes after the author has signed. A C2PA X.509 author assertion or a CAWG identity assertion, if present, is a separate signature and is not this Bind.

  1. The attestor computes contentCommitment as above.
  2. The C2PA claim is assembled (content hash, ingredients, subject, grant pointer, biocid) without the EIP-712 Bind, and the claim generator produces its X.509 signature over those finalized bytes.
  3. The manifestCommitment is computed over those finalized bytes, keccak256(salt || sha256(manifest)), with an independent salt.
  4. The author (for a public work) or the subject (for a work carrying their data) signs the EIP-712 Bind typed data below. The signature covers the contentCommitment, the manifestCommitment, the biocid, the revocable flag, the C2PA ingredientsRoot, the subjectCommit, and the 50-SNP snpBloom, so none of those can change after signing without invalidating the signature. The lab's X.509 claim is separate: Bind is always authored by the subject or the public author.
  5. For a work carrying a subject's data, grant is called with scopeRef equal to the contentCommitment (or a Merkle root containing it), and bindContent records the binding against that grant. For a public work, no grant is created (see Permanence, below).

The Bind typed data. The profile reuses this ERC's EIP-712 domain (name: "ConsentGrant", version: "1", with the chain id and verifying contract). It introduces no ERC-191 scheme. The domain already carries the chain id and verifying contract, so the message carries only the content and policy tuple:

Bind(bytes32 contentCommitment,bytes32 manifestCommitment,string biocid,bool revocable,bytes32 ingredientsRoot,bytes32 subjectCommit,bytes32 snpBloom,uint256 nonce)

manifestCommitment is inside Bind so a relayer cannot substitute a different sealed C2PA after the author has signed. biocid is a string, so EIP-712 includes keccak256(bytes(biocid)) in the digest. That is what stops an attestor from swapping the URI after the author has signed. The raw URI, which in a GenoBank.io deployment embeds the owner's wallet address, is never stored on chain, satisfying Privacy rules 1 and 2. snpBloom is a 50-SNP authorship commitment (zero if the work is not DNA-bound); it is a commitment, never the raw calls. For a public work revocable is false and subjectCommit is zero (the zero-subject profile). For a work carrying a subject's data revocable is true and subjectCommit is the grant's subjectCommit.

An implementation MUST verify the Bind signature with ERC-1271 support (SignatureChecker or equivalent) against an explicit attestor address, so a smart-account subject can author Bind and a signature over different fields cannot bind as a recovered stranger. For a grant-bound work the attestor MUST open subjectCommit. Bind nonces MUST be independent of the grant/revoke nonce, because Bind may be signed before grant consumes that counter. bindContent MUST reject a zero manifestCommitment.

On-chain binding. A conforming binding implementation SHOULD expose:

  • bindContent(bytes32 contentCommitment, bytes32 manifestCommitment, bytes32 ingredientsRoot, bytes32 subjectCommit, bytes32 snpBloom, bool revocable, string calldata biocid, uint256 nonce, address attestor, bytes calldata bindSig, bytes32 grantId, bytes32[] calldata scopeProof, bytes32 subjectSalt). It MUST accept the Bind signature via ERC-1271 against attestor, MUST consume a Bind-specific nonce, MUST reject a commitment that is already bound, and MUST, when grantId is non-zero, require the grant be active, its subjectCommit match, the attestor open that commitment, and contentCommitment be the grant's scope leaf (equality for a single-leaf grant; the same Merkle inclusion checkAccess uses for a root-scoped grant). When grantId is zero it MUST require revocable be false.
  • verifyContentDisclosure(bytes32 salt, bytes32 contentSha256, bytes32 contentCommitment) view and verifyManifestDisclosure(bytes32 contentCommitment, bytes32 salt, bytes32 manifestSha256) view, which recompute the commitment from a disclosed salt and hash.
  • isBindingActive(bytes32 contentCommitment) view, which returns true for a bound public work and, for a bound clinical work, the result of isGrantActive on its grant.

The binding stores commitments only. It MUST NOT store the biocid, the salt, the raw content hash, or any storage URL. Following Privacy rule 5, the binding does not store the C2PA bytes; those are resolved off chain through the manifestCommitment.

URI profile. In the Content Credentials profile the manifest URI MUST be a biocid:// URI (in a GenoBank.io deployment) or another access-controlled resolver under the owner's authority. It MUST NOT be a raw gs:// path, an https://storage.googleapis.com/... URL, or any signed or presigned URL. A signed URL is an ungated, unrevocable, unaudited bearer capability that survives consent withdrawal and cannot be erased, which contradicts this ERC's revocability and erasure guarantees.

Verifier requirements. A conforming verifier, given content, the disclosed salts, the manifest, and an optional grant, MUST treat the failure of any of the following as no valid provenance claim:

  1. if the binding references a grant, isGrantActive(grantId) is true; for a public work with no grant, this check is skipped;
  2. verifyContentDisclosure and verifyManifestDisclosure both return true;
  3. the C2PA hard binding matches the content, and the X.509 signature validates against the verifier's trust list, including certificate revocation;
  4. the binding recovers to the recorded attestor over the Bind digest for this content, chain, and verifying contract.

The on-chain status and the X.509 result combine restrictive-wins. This lattice is normative in this ERC. It is not a companion note. A verifier that implements the profile MUST apply it:

On-chain statusX.509 / C2PA resultVerifier output
ACTIVE, or no grant (public work)validvalid provenance
ACTIVE, or no grantuntrusted, revoked, or expiredno valid claim
REVOKED_BY_SUBJECT / RENOUNCED_BY_GRANTEE / TERMINATED_BY_ISSUER / EXHAUSTED / SUPERSEDEDvalidno valid claim
any terminal statusuntrusted, revoked, or expiredno valid claim

SUPERSEDED is as restrictive as REVOKED_BY_SUBJECT. Untrusted and revoked are both invalid for this lattice. A public work has no on-chain row; only the X.509 and disclosure checks apply, and its claim is permanent.

Permanence. A Revocability.None policy on a grant would contradict this ERC, whose premise is that the subject can always withdraw. So a permanent work is expressed by the absence of a grant, not by a grant that cannot be revoked. A public paper, blog post, or EIP carries a Bind signed with revocable = false and a zero subject and mints no grant; a work carrying a subject's data always carries a revocable grant. The zero-subject profile already defined in this specification is the vehicle for the first case.

Relation to prior work. ERC-7053 indexes content provenance and EAS attests arbitrary claims, but neither carries a revocable consent that a verifier is required to honor restrictive-wins, and neither reuses a purpose-bound consent domain as the authorship domain. The C2PA and CAWG identity-assertion work carries an author identity into a manifest but has no on-chain revocation a verifier must observe. The novelty this profile adds over that body of work is narrow and specific: a mutual, salted, joint content-and-consent binding whose runtime validity is the restrictive-wins combination of an X.509 credential and a revocable on-chain status.

Rationale

Why the token is a status anchor and not the instrument. A signature proves control of a key, not that a particular human assented. Whether an authorization is valid also turns on facts a contract cannot observe, including whether the signer had authority to act for the subject and whether any material representation in the instrument was false. Adjacent work reached the same conclusion: ERC-8328 states that its stored records are "attributable assertions, not proof that the reported action occurred or was lawful," and ERC-8226 marks its own legal-text pointer as non-normative "since its content is not verifiable on-chain." This standard therefore claims weight as evidence and ordering, not legal operative effect, with one exception: revocation status, which relying parties read directly and which is operative for them.

Why not a profile of ERC-8226. ERC-8226 is a two-party delegation keyed (agent, principal), in which the principal owns the asset and the caps are denominated in transfer quantity. This standard is irreducibly three-party, and its subject holds no asset and signs no transaction at exercise time. ERC-8226 also checks compliance at grant time only, accepting a race window between revocation and enforcement mitigated by an out-of-band freeze relay; for an irreversible disclosure, and under regimes whose triggers are knowledge and ease of withdrawal, a grant-time-only check is not sufficient. Finally, ERC-8226's metadata field is expressly non-normative, so the one place a purpose or an instrument could live is disclaimed by its own text. The two compose: an agent may hold an ERC-8226 mandate over a payment asset and a grant under this standard over a data asset.

Why not simply a Verifiable Credential with a status list. For the instrument itself, a W3C Verifiable Credential is the better carrier, and this standard says so. termsRef MUST hash the versioned template referenced from the credential's termsOfUse, never the per-subject credential instance: hashing the instance would be a commitment to personal data and is forbidden by Privacy rule 5. What a VC status list cannot provide is a revocation that is public, consensus-ordered, and not retractable by the issuer. A Bitstring Status List is fetched from an issuer-controlled endpoint that can be rewritten or taken offline. The on-chain revocation is the part that must not be under the issuer's unilateral control, and it is the only part this standard puts on-chain.

Why purpose is a registry code and not a free hash. Purpose vocabularies used in practice are classification systems with parent and child terms, so equality over a hash produces false denials: a grant for a broad permitted use will not match a request labelled with a narrower one that the ontology says it permits. This standard therefore specifies coarse equality over registered codes on-chain, with hierarchy reasoning performed off-chain by the relying party, and requires the registry entry to pin the ontology release so a code's meaning cannot drift. purposeProfileRef carries the hash of the full canonical profile, including any parameters, because several widely used modifiers are parameterized (a disease-specific permission needs a disease, a geographic restriction needs a region, a time limit needs a duration) and no published vocabulary specifies how the parameter attaches. This standard specifies it: the profile document is a JSON object canonical per RFC 8785 (JSON Canonicalization Scheme), containing an array of {term, value} pairs where term is an ontology IRI and value is absent or an IRI or literal. RFC 8785 does not reorder array elements, so two profiles with the same pairs in different array orders would hash differently. Implementations MUST sort the array by term ascending as UTF-8 byte order before canonicalization, and MUST reject profiles whose array is not so sorted. That is a choice made here, not a convention inherited from elsewhere.

Why grants are soulbound. A transferable consent grant is a right of access that can be sold to a person's data. Consent runs to a named recipient; non-assignment is the property, not a limitation. Delegation is expressed by attenuated subGrant, which cannot exceed its parent and cascades on revocation.

Why no per-access event. See Privacy Considerations.

Prior Art

  • ERC-5006 (Final) gives an ERC-1155 token a time-bounded user role. Two-party: the owner grants over their own token. This specification adds the third principal and the purpose.
  • ERC-4907 (Final) is the ERC-721 sibling of the above.
  • ERC-8226 (Draft) delegates scoped, time-bounded, financially capped authority to an agent, keyed (agent, principal). Its caps are denominated in transfer quantity and its metadata field is expressly non-normative, so it has no place to put an instrument or a purpose.
  • ERC-8328 (Review) logs subject-linked compliance events and models a subject, a beneficiary and an actor. It is a reporting interface: its records are "attributable assertions, not proof that the reported action occurred or was lawful", and nothing consults its outcomes.
  • Entriken and Uribe (2020), "Biosample permission token with non-fungible tokens" set out the permitter, permitee and permit model this specification builds on, using ERC-721 to publish permit status and to resolve a permit recursively back to the property owner. It establishes the three-role framing and the public-status requirement. What it does not do is bind a permit to a declared purpose, give the agent an independently revocable leg, or make the permitter's withdrawal operative for a relying party at access time.
  • ERC-8004 (Draft) supplies agent identity. Its agentId is an ERC-721 token id and agentWallet is a reserved, signature-verified field. Implementations SHOULD resolve an EXTERNAL_REGISTRY agent through it rather than defining a parallel agent model.
  • ERC-6617 (Review) defines bit-based permissions. The ops values here are fixed rather than delegated to it because a grant minted by one party is evaluated by another.
  • W3C Verifiable Credentials 2.0 (Recommendation, 2025-05-15) is the better carrier for the instrument itself, and this specification treats it as such: termsRef and purposeProfileRef are hashes of the versioned templates referenced from a credential's normative termsOfUse, not hashes of the per-subject credential and not replacements for it. Only revocation status is placed on-chain, because only revocation must be outside the issuer's unilateral control. A status list is fetched from an issuer-controlled endpoint that can be rewritten or taken offline; a consensus-ordered revocation cannot.

Backwards Compatibility

This standard introduces no changes to existing token behaviour. It is asset-standard agnostic: assetTokenId may reference an ERC-721 or ERC-1155 token, and implementations MAY additionally represent grants as soulbound ERC-1155 tokens in a reserved identifier range. Where they do, the grant token MUST be minted to the grantee only. A subject-side mint MUST NOT be performed, because it would write the subject's account address into transfer state and events and so republish exactly the identifier that subjectCommit exists to keep off-chain. Where a time-boxed user role over an ERC-1155 asset is all that is required, ERC-5006 is sufficient and this standard is unnecessary. Where an agent identity and payment account are required, an implementation SHOULD resolve agent through an ERC-8004 identity registry rather than defining a parallel agent model.

Security Considerations

Enforcement is advisory unless the caller is verified. checkAccess is only as strong as the implementation's verification of msg.sender. An implementation that accepts an agent address as a parameter provides no agent authorization at all. The same failure mode appears off-chain: an eth_call with a client-chosen from does not authenticate the agent. This is stated as a normative requirement above because it is the most likely implementation error, and it is the deployment shape the Motivation describes (a data holder checking a grant before release).

A claimed agent identity is not an authenticated one. ASSERTED_UNVERIFIED exists so that an unproven claim can be recorded honestly rather than disguised as a principal. Implementations MUST NOT treat it as satisfying an agent constraint.

Compromised session keys. Where agentKind == SESSION_KEY, the key authenticates a session, not the software behind it. revokeAgent is the containment primitive, and it is deliberately callable by the agent and its operator as well as the grantee so that containment does not require the grantee to be reachable.

Unbounded delegation. maxDepth and the attenuation invariants exist because a delegation chain without them is privilege escalation. Implementations SHOULD keep maxDepth small enough that isGrantActive remains cheap.

No reinstatement. A function that flips a revoked grant back to active destroys the property that a revocation is final, and would let an operator undo a withdrawal. Re-granting mints a new identifier so that the history distinguishes the two.

Privacy and Data Protection

The requirements in this section bind an implementation exactly as the rest of the Specification does. They are set out separately because violating them harms the data subject rather than the protocol: an implementation may satisfy every other requirement, function correctly, and still place its operator in breach of data protection law.

The chain records authorization state, never facts about the person. Anything that describes the subject rather than the permission belongs off-chain.

The following MUST NOT be written to chain state or events:

  1. Any identifier that carries meaning outside this protocol, in any form, including hashed and including encrypted. This covers an account address, a name, a medical record number, a date of birth, an email address and a biological sequence. A hash of such an identifier remains personal data, and encryption does not remove that status. The single permitted exception is the subject commitment of rule 7, which is generated for one grant and denotes nothing outside it.
  2. An unsalted hash of any identifier drawn from an enumerable space. An address space is enumerable, so a bare hash of an address can be reversed by brute force and yields a pseudonym at best.
  3. Data from which the underlying record can be reconstructed, including partial or aggregate derivatives where the population is small.
  4. Date elements finer than a year, and geography finer than a coarse region.
  5. The plain-language instrument text. termsRef MUST hash a versioned template shared across many subjects, never a per-subject executed document, which would be a commitment to personal data.
  6. Free-text purpose or condition strings. Use registered codes and the profile hash.
  7. A stable subject identifier reused across grants. The subject MUST be referenced on-chain only as subjectCommit, a commitment to a subject key that is generated for that grant, drawn from at least 128 bits of entropy, and derived from no pre-existing identifier. A fresh key MUST be used for each grant, so that two grants concerning the same person are not linkable by an observer. This is a pseudonym and nothing stronger: it remains personal data while it can be linked to a person, and the erasure argument rests on destroying the key, not on the commitment being free of linkage.
  8. Session identifiers or other behavioral identifiers. A session identifier has no on-chain enforcement value, because a contract cannot verify that a session existed, and publishing one makes an individual's interactions linkable.

Per-access events are prohibited for this reason. An event stream keyed to a subject publishes an access-pattern timeline. Access frequency and timing are themselves revealing, and on a permissioned chain with a known participant set they are attributable. The hash-chain-plus-commitment design preserves tamper-evidence and ordering over the records that were appended, while publishing only a periodic head and a count. It does not prove that every access was recorded.

Deployment. Implementations SHOULD deploy on a permissioned network with an identifiable operator, because it permits a clear allocation of controller responsibility. On a permissionless network there is a live argument that validators become joint controllers of the personal data in the state and logs they process, which is a materially worse position for the subject and for the operator.

The governing constraint. If a deployment cannot render every on-chain artifact anonymous by deleting off-chain material, then consent may not be an available lawful basis for it in some jurisdictions. Implementers SHOULD verify that this holds before relying on consent, rather than after.

subjectCommit is retained in state so subject-authored acts can be authorized without publishing an account address. Implementations that support a zero-subject profile (no on-chain subject authentication, revocation only by an off-chain process) MAY leave subjectCommit as zero and MUST then reject every on-chain subject-signed path for that grant. The zero-subject profile is buildable because subjectOf(grantId) is published.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth