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

I would like feedback on a draft ERC before I open a PR.

The one-line version. Every delegation standard I have found on the Standards Track is two-party: the party who consents is the party who benefits, and the party at risk is the party who signed. I want to standardize the case where those are different people, because that is the shape of consent to use someone’s data.

Disclosure: I am the founder of GenoBank.io, which builds a decentralized biobanking system in which patients grant researchers access to data about themselves, and this draft comes out of that work. The draft is deliberately domain-neutral: nothing in the normative text mentions health, genomics, or GenoBank.io, because the structure is not specific to any of them.


The problem

The case has three principals, and existing standards collapse the last two:

  • a subject, the person whose data the grant concerns and whose assent it records
  • a grantee, the party authorized to exercise the grant
  • an agent, an optional principal that exercises the grant on the grantee’s behalf

In ERC-4907 and ERC-5006 an 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 consenting party is the beneficiary. 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.

Three gaps follow, none of which I could express with what exists:

1. The subject is not the beneficiary. ERC-8328 does model a subject, and so does W3C VC 2.0 with credentialSubject. What neither does is make a non-party subject’s withdrawal operative for a relying party at access time. ERC-8328 says this itself: it “does not define compliance policy… transfer restrictions”, and its OUTCOME_REVOKED is a recorded assertion that nothing consults.

2. The agent must be revocable separately from the human. Where an agent is modelled at all, it is modelled as an extension of the principal, so no two-party standard can 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.

3. Purpose is not enforced anywhere. Every regulatory-compliance ERC to date (3643, 7518, 8106, 8226, 8320) encodes securities vocabulary: accreditation, jurisdiction blocks, AML flags, transfer caps. A fund unit has no purpose limitation, so nobody needed one. Data does.


Why on-chain at all

This is the question I expect to be pressed on, and the answer has two parts.

For the instrument itself, a W3C Verifiable Credential is the better carrier and the draft says so. termsRef and purposeProfileRef are hashes of documents referenced from a credential’s normative termsOfUse. They are not a replacement for it.

The one thing that must not live in an issuer-controlled place is revocation status. A Bitstring Status List is fetched from an endpoint the issuer can rewrite, take offline, or backdate; a consensus-ordered revocation cannot be any of those.

That matters concretely rather than theoretically. The incumbent standard for credentialed access to controlled datasets is GA4GH Passports, and its own AAI profile states that a Visa Issuer “MAY provide tokens of this type without any revocation process”, defines exp as explicitly not a hard cutoff (“access is NOT necessarily removed by the exp timestamp”), and rate-limits the only active check: polling “MUST NOT be done more than once per hour per Passport Clearinghouse.” There is no revocation list and no push channel. Worst-case propagation of a withdrawn consent is bounded by token lifetime and an hourly poll.

So the claim is narrow and I want it judged narrowly: public, non-repudiable, consensus-ordered revocation of a grant whose subject is not its beneficiary. It is not a new identity system, not a new credential format, and not a compliance oracle.


Why not a profile of ERC-8226

ERC-8226 is the closest neighbour and I want to be precise about the difference rather than strawman it.

Its Mandate is keyed (agent, principal) and its caps are denominated in transfer quantity. Three things stand in the way of a profile:

  • The principal is the beneficiary. There is no seat for a third person, and adding one changes the key.
  • Compliance is checked at grant time only. Its own Security Considerations accept a race between revocation and enforcement, mitigated by an out-of-band freeze relay. For an irreversible disclosure, under regimes whose triggers are knowledge and ease of withdrawal, a grant-time check is not sufficient.
  • metadata, the only field where an instrument or a purpose could live, is expressly non-normative: implementations “MUST NOT rely on it for enforcement decisions.”

The two compose: an agent may hold an 8226 mandate over a payment asset and a grant under this spec over a data asset. I do not think that makes either a profile of the other.


Status

Draft ERC plus a reference implementation, deployed and verifiable:

chain address
Avalanche mainnet (43114) 0xA04d20E6fB15A0939B08eb1cB88673f34051342c
Avalanche Fuji (43113) 0x25d419259d7336a4d883808adc02a5F92520b2C0
Sequentia (15132025, permissioned) 0x473Cd8D24437c9321e2e0c32C7cC9F777f99d1A5

45 tests, mostly negative ones. The interesting assertions are that a sub-grant cannot exceed its parent on scope, ops, expiry, purpose or uses; that a claimed-but-unproven agent identity never satisfies an agent constraint; that revoking the agent leg leaves the human grant alive; and that revocation cost does not grow with the number of delegated children.

Before drafting this post I ran the spec and the implementation through an adversarial review from five independent perspectives: an EIP editor, a security auditor, a data-protection officer, a hostile competing-standards author, and an implementer working from the text alone. It produced 54 findings, 48 of which survived a verification pass. I fixed the 7 it classed as publication blockers. Two of those were cases where my own NatSpec asserted something the code did not do, and one was a live privilege escalation in the deployed contract. I mention this because the remaining items below are not modesty; they are a list I already have.


What I know is still weak

I would rather name these than leave them to be found.

The privacy section contradicts the data model. The Privacy rules say personal data MUST NOT be written on-chain in any form including hashed. The data model mandates address subject. The draft recommends a zero-subject profile where the subject is derived from an asset registry, but the published interface has no subjectOf hook, so that profile is not buildable as specified. Separately, if grants are represented as ERC-1155 tokens, the subject-side mint republishes the address that zeroing the field was meant to remove, and nonces is keyed by the raw address. I have not resolved this and it is the single thing I most want input on.

usesMax is a self-declaration. Only the grantee or agent advances useCount, via commitUsage. A grantee who never calls it holds an effectively uncapped grant. It binds contractually, not cryptographically, and the draft should either say so plainly or split checkAccess into a view and a state-changing consume.

The usage hash chain is under-specified. h_i = H(h_{i-1} || record_i) with no named H, no h_0, and no record_i schema. Two implementations cannot produce comparable chains. The draft still uses the word “completeness”, which is wrong: a hash chain gives tamper-evidence and ordering; it gives no protection at all against a party who simply declines to append.

Merkle leaf construction is unspecified (hash function and pair ordering), and a withdrawn leaf is not inherited by sub-grants, so per-datum withdrawal does not survive one delegation hop.

Four Status values have no setter or transition rule, and SUSPENDED is documented as reversible, which contradicts the no-reinstatement rule.

checkAccess authenticates only an on-chain caller. An off-chain relying party calling it via eth_call supplies from itself and gets no authentication whatsoever. Given that the deployment I describe in the Motivation is exactly an off-chain gateway, this needs either a signed-challenge companion or a much louder warning, and probably both.

RFC 8785 does not sort array elements, so the purpose profile canonicalization I specify does not produce a stable hash for the same semantic profile.


What I am asking

  1. Is the narrow claim actually unclaimed? I am asking specifically about a non-party subject’s withdrawal being operative at access time, not about three-party modelling in general, which ERC-8328 already does.
  2. Should the ops bitmask be ERC-6617, or are fixed values right when a grant minted by one party is evaluated by another?
  3. On the privacy contradiction above: is a subjectOf resolver hook the right shape, or should the subject leave on-chain state entirely and live only in the off-chain credential?
  4. Is there appetite for this as a standard at all, or is the correct outcome that it should be a VC profile with an on-chain status anchor and nothing more? I would rather hear that now than after a PR.

If the framing itself is wrong, that is the answer I would most like to have.


Links

Deployed reference implementation, byte-identical across all three (15,385 bytes runtime):

Superseded instances from an earlier deployment remain live on all three chains and carry a
scope-containment bypass that this version fixes. They are not proxies and cannot be
disabled, so I am naming them rather than leaving them to be found:
0xA25543a0eDF4755d7C4f39207F8cF59C23173864 (mainnet),
0xEE53dAAf7AF86E47bc3155b0642c41a30F1A5d06 (Fuji),
0x6D6604eD95A46018a36C730614714f9586049C3D (Sequentia). Do not use them.

Full draft text follows in the next reply, so the thread is self-contained and does not
depend on a link staying alive.

Full draft text, posted inline so the thread stands on its own.

Two preamble values are still placeholders by necessity: discussions-to becomes this
thread’s URL, and eip becomes the PR number once the PR is open. Everything else is final
as far as I can take it without feedback.


eip: <TBD, assigned by an EIP editor>
title: Purpose-Bound Third-Party Data Consent
description: A revocable, purpose-bound consent grant in which the consenting subject is not the beneficiary, with an independently revocable agent leg.
author: Daniel Uribe (@duribebe)
discussions-to: <Ethereum Magicians thread, required before a number is assigned>
status: Draft
type: Standards Track
category: ERC
created: 2026-07-30
requires: 165, 712, 1271

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-repudiable, 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.

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 modelled, it is modelled
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-repudiable, 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 organisation 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  reversible, incident response only
    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 canonicalized 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
    address subject;
    uint48  notBefore;
    uint48  expiresAt;
    // slot 6
    address grantee;
    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 7
    address agent;
    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:

bit name meaning
0x01 READ retrieve the resource
0x02 QUERY evaluate over it without retrieving it
0x04 DERIVE produce a new artifact from it
0x08 EXPORT move it outside the enforcing boundary
0x10 TRAIN use it to fit model parameters

Bits 0x20 and above are reserved. An unrecognised 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, address 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;
    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;

    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 nonces(address subject) external view returns (uint256);

    function revocationEpoch() external view returns (uint256);

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

Required behaviour

Authorship of consent. grant MUST require either msg.sender == g.subject, or a valid
EIP-712 signature by g.subject, verified with ERC-1271 support for
contract accounts. An implementation MUST NOT permit an operator, issuer, or minter to create a grant
on a subject’s behalf without such a signature.

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,
      address subject,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:

bits field type
0 to 47 notBefore uint48
48 to 95 expiresAt uint48
96 to 127 ops uint32
128 to 159 purposeCode uint32
160 to 175 usesMax uint16
176 to 183 agentKind uint8
184 to 191 flags uint8

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, MUST expose it as nonces(address), 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 subject equals parent
subject; 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.

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. When the flag is clear, scopeRef MUST equal resourceHash.

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.

Action Authorized caller Resulting status
revokeBySubject the subject only REVOKED_BY_SUBJECT
renounceByGrantee the grantee RENOUNCED_BY_GRANTEE
revokeAgent grantee, agent, or agent operator agentStatus only; grant survives
revokeScopeLeaf the subject grant survives, leaf withdrawn

revokeBySubject MUST be unconditional, MUST take effect immediately, MUST NOT be blockable 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 unexecutable and defeats the “MUST NOT be blockable” rule in the same sentence that
states it. An
implementation MUST NOT provide any function that reinstates a revoked grant; re-granting MUST mint a
new grant with a new identifier.

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 h_i = H(h_{i-1} || record_i), whose head is committed periodically via
commitUsage. See Privacy Considerations.

(Draft continues in the next post: Rationale, Prior Art, Backwards Compatibility, Security and Privacy Considerations.)

Draft text, part 2 of 2. Part 1 above carries the preamble, Abstract, Motivation and Specification.


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 evidentiary weight
and ordering, not legal operativeness, 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 references one.
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
ontologies with subsumption hierarchies, 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 canonicalized 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 canonicalized per
RFC 8785, containing an array of {term, value} pairs where term is an ontology IRI and value is
absent or an IRI or literal. That is a choice made here, not a convention inherited from elsewhere.

Why grants are soulbound. A transferable consent grant is a saleable right of access to a
person’s data. Consent runs to a named recipient; non-assignability 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.
  • 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 documents referenced from a credential’s normative termsOfUse, 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 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. This is stated normatively above because it is the
most likely implementation error.

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. Personal data in any form, including hashed and including encrypted. A hash of personal data
    remains personal data, and encryption does not remove that status.
  2. An unsalted hash of any identifier drawn from an enumerable space. An address space is enumerable,
    so a bare hash of an address is invertible by brute force and is pseudonymisation 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. Implementations SHOULD use a per-grant salted
    pseudonym so that two grants concerning the same person are not trivially linkable.
  8. Session identifiers or other behavioural 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 disclosive, and on a permissioned
chain with a known participant set they are attributable. The hash-chain-plus-commitment design
preserves tamper-evidence, completeness, and non-repudiation while publishing only a periodic head
and a count.

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.

subject is retained in state to make revokeBySubject cheap to authorize. Implementations that can
derive the subject from the asset registry SHOULD do so instead and leave the field zero, which
removes a category of personal data and frees a storage slot.

Copyright

Copyright and related rights waived via CC0.

I1 resolved.

The contradiction is not address subject. Privacy rule 1 bans personal data on-chain “in any form, including hashed”. Rule 7 recommends a per-grant salted pseudonym, which is hashed personal data. Rule 1 forbids what rule 7 recommends, so rule 1 is unsatisfiable by any design that authenticates a subject on-chain at all.

Rule 1 now covers only identifiers with meaning outside the protocol: account address, name, medical record number, date of birth, email, sequence. Rule 7 becomes the normative subject reference.

was now
address subject bytes32 subjectCommit: per grant, 128 bits minimum, from no pre-existing identifier, never reused
no hook, so the zero-subject profile was unbuildable subjectOf(bytes32) returns (bytes32)
nonces(address) nonces(bytes32)
ConsentRevoked(..., address indexed by, ...) bytes32 indexed by
ERC-1155 subject-side mint permitted prohibited, mint to the grantee only

revokeBySubjectWithSig now verifies against the commitment rather than msg.sender. A subject with no wallet, no gas and no account can revoke.

Limits, stated rather than found: this is a pseudonym, not anonymisation. It stays personal data while it can be linked, and erasure rests on destroying the key. It hides the subject from observers, not from the grantee, who holds the credential.

The three deployed contracts still hold subject as an address and no longer match the spec. Read them for the delegation, containment and revocation semantics only.

Prior art I should have listed in the opening post: Entriken and Uribe, Biosample permission token with non-fungible tokens (2020). It establishes the permitter, permitee and permit roles and the public-status requirement, using ERC-721 to resolve a permit back to the owner. It does not bind a permit to a purpose, give the agent a separately revocable leg, or make withdrawal operative at access time. Those three are what this draft adds.

Questions 1, 2 and 4 stand.

Why this needs a graph and not a flag.

A biosample is not one object. It becomes a chain of derived artefacts, each produced by a different party, each further from the person than the last.

One consent decision at the left has to govern seven transformations and four custodians. Nobody re-asks the person at any hop. That is the case existing two-party delegation standards cannot express: by the time the data reaches the model provider, the party at risk is not a party to the transaction.

So consent has to travel with the artefact and narrow as it goes. Each hop mints a sub-grant that can restrict scope, purpose, expiry and use count, and can never widen them.

That is the attenuation rule in the spec, and it is why the containment test matters: a sub-grant proving a Merkle leaf outside its parent’s root is refused at mint.

Withdrawal then runs the graph in reverse, and stops at exactly one place.

Everything up to the vectors is deletable. Model weights are not. This is the whole reason the draft treats permanence as a measurement problem rather than repeating a deletion promise the technology cannot honour: once a contribution is absorbed into weights, the only honest remedy left is to have measured what it added and to account for it.

The three diagrams are the same argument at three scales: the chain is long, consent must compose along it, and revocation has a hard edge.

**Open items closed. Number is ERC-8356.**

@abcoathup assigned **8356** and retitled this thread. The PR is

Add ERC: Purpose-Bound Third-Party Data Consent by duribebe · Pull Request #1921 · ethereum/ERCs · GitHub (`ERCS/erc-8356.md`). The branch

name still says 8355 so the open PR is not detached; only the file and preamble

carry the number.

The items I had flagged rather than defended are now normative in the draft.

Short form of each:

1. **`usesMax` honesty.** It is a contractual ceiling. Only the grantee or agent

advances the off-chain chain and calls `commitUsage`, so the constrained party

self-reports. A relying party that needs a bound the grantee cannot under-report

must meter outside this mechanism. Said so in the text.

2. **Usage hash chain.** `H = keccak256`, `h_0 = bytes32(0)`, and

`record_i = keccak256(abi.encode(grantId, accessedAt, op, resourceHash, requestId))`.

The word “completeness” is gone: a party that declines to append leaves no gap

a third party can detect from the head alone.

3. **Merkle construction.** Leaf is

`keccak256(bytes.concat(keccak256(resourceId)))`, interiors are sorted-pair

(OpenZeppelin `MerkleProof` / StandardMerkleTree). A leaf withdrawn via

`revokeScopeLeaf` is consulted on **this grant and every ancestor**, so a

per-datum withdrawal survives a delegation hop.

4. **Status transitions.** Full table. `suspend` / `resume` / `terminateByIssuer`

are in the interface. `resume` of `SUSPENDED` is the **only** return to

`ACTIVE`; every other non-ACTIVE status is terminal. Subject withdrawal still

works from `SUSPENDED`.

5. **`checkAccess` and `eth_call`.** Stated explicitly: the function reads

`msg.sender`. An off-chain `eth_call` with a client-chosen `from` authenticates

nothing. Off-chain enforcement must combine `isGrantActive` /

`effectiveStatus` with an independent authentication of the agent or grantee.

On-chain gates in the same transaction remain sound.

6. **`termsRef` vs VC instance.** MUST hash the **versioned template** referenced

from the credential’s `termsOfUse`, never the per-subject credential instance

(Privacy rule 5).

7. **Purpose profile ordering.** RFC 8785 does not reorder arrays. The

`{term, value}` array MUST be sorted by `term` (UTF-8 ascending) before

canonicalization, or two equal profiles hash differently.

8. **Subject commitment opening.** Normative

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

`keccak256(subjectKey)` is forbidden. Authorship and every subject-signed path

prove knowledge of that opening.

Also cleaned leftover wording that still said `g.subject` / "child subject equals

parent subject" after the commitment change.

Reference implementation source is updated to match (suspend/resume/issuer

termination, ancestor leaf checks, event `by` as `bytes32`). The three already

deployed addresses remain the pre-revision bytecode; redeploy is separate so this

ERC PR stays a pure text change. 48 tests passing on the revised contract.

Happy to take further review on the PR or here.

**Reference implementation redeployed** (open-item revision).

Runtime 16,892 bytes. `suspend` / `resume` / `terminateByIssuer`, ancestor leaf inheritance, and `bytes32` `ConsentRevoked.by` are live. Purpose registry is zero (fail-closed). 48 tests on the revised source.

| chain | address |

|—|—|

| Avalanche mainnet (43114) | `0x360ac376e20B4d930da810f7CeA935F52693a84C` |

| Avalanche Fuji (43113) | `0xc92f9f1D68A445189Ad3ad28524186A11Be30DcA` |

| Sequentia (15132025) | `0xD526E4d6e449A091D18900DC9ACb183e9556F714` |

Prior instances remain live and are recorded as superseded. They lack the open-item surface; do not use them for new integrations.

PR: Add ERC: Purpose-Bound Third-Party Data Consent by duribebe · Pull Request #1921 · ethereum/ERCs · GitHub