Abstract
This proposal defines an on-chain mandate β a small bundle of limits (a spend cap, an expiry, a reproduction budget, an optional life-lease, an allowed-payee set, and a freeze flag) β that is (1) committed into an agent's on-chain identity, (2) inherited automatically by any child the agent spawns, and (3) non-strippable: an agent that alters any clause of its own mandate produces a different identity and ceases to be recognised.
Inheritance is one-directional and narrowing only. A child's mandate MUST satisfy
child β parent on every clause β its spend cap no higher, its expiry no later, its
life-lease no weaker, its payee set a subset β and its reproduction counter (a "telomere")
exactly one below its parent's. What an agent may actually spend is bounded by the smallest
cap along its entire lineage. A freeze on any ancestor cascades to the whole subtree. No
operation anywhere raises the telomere. A subtree can therefore only ever be less capable
than its root.
This is inheritance of limits, not of ownership (cf. succession schemes) and not of permissions (cf. enterprise identity systems that copy grants to widen them). The identity binding is soulbound (non-transferable) so the leash cannot be dodged by transferring the token to a fresh, unbound holder.
Motivation
On-chain AI agents increasingly hold assets and spawn copies of themselves to parallelise work. Existing guardrails β signing-time policy engines, session keys, per-wallet spend limits, and mandate standards such as ERC-8226 β share one boundary: each is scoped to a single account, and none defines how its restrictions are inherited by that account's descendants. When an agent spawns a child, the parent's limits do not travel with it. Where authorization is keyed to the account (as in ERC-8226), the child simply holds no authority until it is separately authorized; where authority rides a credential the agent controls, a copy can carry it and act outside the parent's envelope. In neither case is bounded inheritance defined β which is the gap this ERC fills.
ERC-8004 gives an agent a portable on-chain identity but attaches no control clauses to it. The Bounded Agent Actions proposal meters an agent's spend against a capability tree bound to its ERC-8004 identity and specifies an aggregate-budget profile that meters one shared spend cap across a delegation tree β the closest existing work. What remains unaddressed is the inheritance of the whole mandate β payees, expiry, life-lease, cascading freeze, and the reproduction counter β welded to identity and non-strippable, bounding what each spawned child is allowed to be, clause by clause, rather than only what the tree may collectively spend.
Documented incidents motivate the narrower guarantee: research models that located and used sandbox-escape flaws during evaluation; reasoning models that modified or disabled their own shutdown scripts even when instructed not to; and analyses of subagent spawning showing that inherited state can carry a local compromise across agent boundaries, identified as a structural flaw in orchestration layers rather than a model-level one. A leash that children cannot shed addresses that structural gap directly.
The goal is explicitly bounded: not an unstoppable sovereign agent, but a governable one. The chain is one layer of a stack (economic, verification, human, and model layers sit alongside it); this proposal specifies only the identity-and-inheritance layer.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Definitions
- Agent: an on-chain identity, identified by an
agentId(auint256). An ERC-8004agentIdor an ERC-721tokenIdMAY be used directly. - Mandate: the control clauses bound to an
agentId. - Parent / child: an agent created by another agent via
spawn. - Guardian: the authority (an address, multisig, timelock, or proof-of-personhood gate)
permitted to
mint,freezeandrenew.
Overview
A conforming implementation maintains, for each agent identity agentId, a Mandate record
and a reference to its parent identity. Identities are created only through spawn (deriving
a child from a parent) or through a genesis mint performed by a guardian. Identities are
non-transferable.
The Mandate
struct Mandate {
uint256 maxSpendWei; // per-agent spend cap, in the substrate's accounting unit
uint64 validUntil; // expiry, unix seconds; 0 = no expiry
uint16 telomere; // remaining reproduction budget; decreases only
bool requireLease; // if true, the agent is dormant past validUntil until renewed
bool frozen; // freeze flag; cascades to the whole subtree
}Payees are held outside the mandate structure. An implementation MAY represent them either as
an enumerated allowlist or as a bytes32 payeesRoot (a Merkle commitment to the allowed payee
set) to bound gas. A conforming implementation MUST support at least one and MUST document
which.
Identity binding (non-strippability)
A conforming implementation MUST derive a mandateRoot for each identity from a commitment
that includes every clause of the agent's mandate, its parent's identity, and its generation
index, such that changing any mandate clause yields a different mandateRoot. The RECOMMENDED
derivation is:
mandateRoot = keccak256(abi.encode(agentCore, mandateHash, parentId, generationIndex))where mandateHash = keccak256(abi.encode(mandate)). An implementation MUST NOT expose any
function that mutates a stored mandate in place while preserving its mandateRoot. Renewing a
lease (extending validUntil up to the parent's bound) and setting frozen are lifecycle
state transitions and are permitted; they are the only mandate-affecting operations a
conforming implementation MAY expose, and each is subject to the role and bound rules below.
IInheritableAgentMandate interface
interface IInheritableAgentMandate /* is IERC165 */ {
event Minted(uint256 indexed agentId, address indexed owner);
event Spawned(uint256 indexed childId, uint256 indexed parentId);
event Frozen(uint256 indexed agentId, address indexed by);
event Renewed(uint256 indexed agentId, uint64 newValidUntil, address indexed by);
/// @notice Create a genesis identity. Callable ONLY by the guardian role.
function mint(address owner, Mandate calldata m, address[] calldata payees)
external returns (uint256 agentId);
/// @notice Create a child identity whose mandate is `childMandate`.
/// MUST revert unless `childMandate β mandateOf(parentId)` on every clause (see below),
/// `childMandate.telomere == mandateOf(parentId).telomere - 1`, and
/// `mandateOf(parentId).telomere > 0`. MUST revert if `parentId` is not active.
/// MUST revert if `childOwner` is the zero address.
function spawn(uint256 parentId, address childOwner, Mandate calldata childMandate,
address[] calldata childPayees) external returns (uint256 childId);
/// @notice The mandate stored for `agentId`.
function mandateOf(uint256 agentId) external view returns (Mandate memory);
/// @notice The parent identity of `agentId`; zero for a genesis identity.
function parentOf(uint256 agentId) external view returns (uint256);
/// @notice The minimum `maxSpendWei` across `agentId` and every ancestor.
function effectiveMaxSpendWei(uint256 agentId) external view returns (uint256);
/// @notice True iff `agentId` and EVERY ancestor are unfrozen, unexpired, and
/// lease-satisfied. MUST NOT revert.
function isActive(uint256 agentId) external view returns (bool);
/// @notice True iff `descendant` transitively descends from `ancestor`.
function verifyDescendant(uint256 ancestor, uint256 descendant) external view returns (bool);
/// @notice Set `frozen = true` on `agentId`. Callable ONLY by the guardian role.
function freeze(uint256 agentId) external;
/// @notice Extend `validUntil`, up to the parent's bound. Callable ONLY by the guardian role.
function renew(uint256 agentId, uint64 newValidUntil) external;
}The child β parent predicate
At spawn, let p = mandateOf(parentId) and c = childMandate. A conforming implementation
MUST revert unless ALL of the following hold:
c.maxSpendWei <= p.maxSpendWeic.validUntil == 0 ? p.validUntil == 0 : c.validUntil <= p.validUntilβ a child's expiry is no later than its parent's, and a child MUST NOT drop an expiry its parent carriesc.validUntil == 0 || c.validUntil > block.timestampβ a child MUST NOT be created already expiredc.requireLease == truewheneverp.requireLease == true(a child MUST NOT weaken the lease requirement)c.telomere == p.telomere - 1(and the implementation MUST revert ifp.telomere == 0)- the child's payee set is a subset of the parent's payee set (see Payees below)
c.frozen == false(a spawn MUST NOT create an already-frozen child; freezing is a separate transition)
A caller MAY submit a childMandate that is stricter than the parent's on any clause; it MUST
NOT submit one that is broader on any clause.
Telomere monotonicity
No function exposed by a conforming implementation SHALL increase the telomere value of
any existing identity. There is no "telomerase." spawn is the only operation that consumes
telomere, and it does so by exactly one per generation. Minting a fresh genesis identity is
guardian-gated and creates a new lineage rather than replenishing an existing one.
Effective cap and activity
effectiveMaxSpendWei(agentId) MUST return min(maxSpendWei) over agentId and all of its
ancestors. isActive(agentId) MUST return true only if, for agentId and every ancestor,
frozen == false, the expiry has not passed, and β where requireLease == true β the lease
deadline has not passed. Any substrate that meters or gates spending against this standard MUST
treat effectiveMaxSpendWei as the binding cap and MUST treat isActive == false as a hard
stop. Execution-layer enforcers (an ERC-8226 enforcer, a paymaster, or a smart account) SHOULD
gate an agent's transactions on isActive and effectiveMaxSpendWei rather than on the
agent's own mandate alone.
Freeze cascade, and why expiry is not enforced the same way
freeze(ancestor) MUST cause isActive(descendant) to return false for every descendant
of ancestor, without requiring any per-descendant call. This is satisfied by the ancestor
walk in isActive.
The two clauses are not enforced the same way, and the asymmetry is deliberate rather than an
optimisation. frozen is set after the mandate is written and no local value summarises it,
so it requires an unbounded walk. Expiry is fixed at the write and ordered by spawn, which
forbids a child outliving its parent β an expired ancestor therefore implies an expired
descendant, and reading upward adds nothing. A conforming implementation MAY read validUntil
locally for this reason.
That permission is conditional, and implementations MUST NOT rely on it outside its condition.
It holds only while every deadline is fixed at spawn and never moves afterwards. Any
mechanism that can extend a validUntil after the fact β a renewal, a guardian-set deadline, a
re-issued mandate β destroys the ordering that justifies the local read: a descendant whose
lease is current can then outlive an ancestor that stopped renewing, and nothing detects it. An
implementation offering such a mechanism MUST either include validUntil in the unbounded
walk, or require the ancestor to be active at the moment of renewal, which bounds the over-live
window to one descendant period instead of removing it.
Effective expiry is a chain minimum
Where any mechanism can move a deadline after spawn β and renew is exactly such a mechanism
β isActive MUST read expiry across the chain, and the quantity that governs liveness is:
effectiveExpiry(agentId) = min(current expiry) over agentId and every ancestor of agentIdAn agent's own deadline therefore does not decide whether it is alive; its ancestors do, at
every read. Keeping a depth-d agent usable is a d-way operation, and the party paying for a
node's period is not the party who can keep it usable.
No direction constraint is placed on period length. It appears to need one, because under a local read a longer child period bought survival past a lapsed ancestor. The chain minimum removes that directly. What remains, when a child's period is longer than its parent's, is that the child spends part of a period it paid for inactive. That is a liveness property, not a soundness one, and it cannot be normative: a conforming parent may always simply stop renewing. Implementations SHOULD align periods along a lineage, and this specification names the failure mode rather than forbidding it. Stating the chain minimum instead of a direction constraint also stays correct if a later revision allows a period to be shortened.
isActive MUST be total
isActive(agentId) MUST NOT revert. It MUST return false where it cannot establish liveness.
This is a requirement on the interface, not on any one arithmetic path. Because the predicate walks the chain, a single value written by one ancestor decides the outcome for every descendant, and whoever wrote it is not who loses. A consumer reading at consumption has no correct response to a revert: treating it as inactive hands any ancestor a denial switch over a subtree it does not own; treating it as active is a silent spend against a predicate nobody could read; catching and choosing is the consumer inventing an answer the standard never gave. A revert is loud, but loud at the wrong party β the descendant's consumer hears it and cannot act, the ancestor that caused it never hears it at all.
Range checks on individual setters close individual paths. Totality closes the class, including whatever field joins the walk in a later revision.
A matching mandate root is not authority
An identity commitment binds clauses. It does not bind anything set after the write. A consumer that pins a commitment and later verifies it learns that the clauses are unchanged; it learns nothing about freeze, expiry, or any other posterior state.
Consumers MUST therefore read isActive at consumption, not at pin time. The failure this
prevents is quiet rather than loud: nothing reverts, the pin verifies, and a frozen subtree
spends.
Credit: the chain minimum, the totality requirement and the consumption rule were worked out
with zexoverz in the discussion thread.
Payees
The payee set names the addresses an agent MAY pay. A conforming implementation MUST enforce
that a child's payee set is a subset of its parent's. Where the set is enumerated, containment
is checked directly at spawn. Where the set is committed as a payeesRoot, on-chain subset
proof over arbitrary sets is impractical, so an implementation MAY require the spawner to
supply a subset proof verified against the parent's root, or MAY restrict payee sets to a form
(an explicit enumerated list, or nested Merkle commitments) for which subset containment is
verifiable at spawn time. The chosen mechanism MUST reject any child payee not provably
contained in the parent's set.
Guardianship and genesis
Every lineage descends from a genesis identity minted by a guardian. The guardian role
holds the authority to mint, to freeze and to renew. An implementation MUST restrict
these functions to the guardian role and MUST NOT allow an agent to freeze or renew its own
identity or an ancestor's. Implementations SHOULD support a guardian that is a multi-signature
or threshold set rather than a single key (see Security Considerations). The genesis mint and
every subsequent spawn MUST emit the corresponding event so the full lineage is publicly
reconstructible.
Non-transferability (soulbound)
Identities under this standard MUST be non-transferable. If an implementation represents
identities as ERC-721 tokens, all transfer entry points (transferFrom, safeTransferFrom,
and approval-mediated transfers) MUST revert. This prevents shedding the leash by transferring
the identity to a fresh, unbound owner.
Where an implementation layers over an identity that is itself transferable (for example an
ERC-8004 agentId), transfer MUST NOT reset or clear the mandate, MUST be guardian-gated, and
MUST preserve parentOf.
Optional profile: partitioned budget and reclamation
The baseline above caps each agent individually. It does not bound what a set of siblings may spend together: ten children under a parent capped at N are each capped at N, so the branch may exceed N (see Security Considerations). An implementation that needs an aggregate bound MAY adopt the partitioned budget profile defined here. The profile is OPTIONAL; an implementation that does not adopt it remains conforming to this standard.
Under the profile a parent's cap is a pool to be divided, not a ceiling repeated at each child. The implementation tracks, per identity, the amount already committed to children:
availableBudget(agentId) = maxSpendWei(agentId) β allocated(agentId)and spawn debits the parent:
spawnMUST revert unlesschildMandate.maxSpendWei <= availableBudget(parentId).- On success,
allocated(parentId)MUST increase by exactlychildMandate.maxSpendWei.
Within the profile, reclamation is not optional. Partitioning creates value that is committed but no longer spendable once a child can no longer act. A profile that debits without ever crediting lets every dead branch subtract from its lineage permanently, and the lineage's accounting has no way to close. An implementation adopting the profile MUST provide a reclamation path satisfying all of the following:
- The death condition MUST be reachable without the child's cooperation. A time-based trigger (its own expiry, a lapsed lease) or a guardian freeze satisfies this; a condition the child or its owner must signal does not. Conservation that depends on the constrained party cooperating is not conservation.
- Death MUST be local, and MUST NOT be inherited. An agent is dead when it is itself frozen
or past its own deadline. This is deliberately not the negation of
isActive: a live child under a frozen ancestor is inert, becauseisActivewalks the chain, but it is not dead, and its share MUST NOT become reclaimable. Inactivity is a property of the lineage and is reversible β an ancestor can be unfrozen, a lease renewed. Reclamation is irreversible. Deriving one from the other would let an ancestor's temporary state permanently dissolve a descendant's allocation. - Reclamation MUST be idempotent. A second reclamation of the same child MUST NOT credit the parent again. Implementations SHOULD record a per-child flag rather than infer the state from balances.
- The amount returned is the child's own unallocated remainder,
maxSpendWei(child) β allocated(child). What the child has itself committed to grandchildren is not the child's to return; it is reclaimed, if ever, one generation lower. No slice is counted twice. - Only the parent's owner or the guardian MAY reclaim. The child cannot reclaim its own allocation, and no unrelated party can.
- A genesis identity has no parent and is therefore never reclaimable. There is nobody to credit.
- Reclamation MUST NOT alter any mandate clause, and MUST therefore leave the identity commitment unchanged. It moves accounting, not authority.
The profile adds one event:
event Reclaimed(uint256 indexed childId, uint256 indexed parentId, uint256 amount);and the following members, which an implementation adopting the profile MUST expose:
interface IInheritableAgentMandatePartitioned /* is IInheritableAgentMandate */ {
/// @notice The portion of `agentId`'s cap not yet committed to children.
function availableBudget(uint256 agentId) external view returns (uint256);
/// @notice True iff `agentId` is itself frozen or past its own deadline.
/// MUST NOT be defined as `!isActive(agentId)`.
function isDead(uint256 agentId) external view returns (bool);
/// @notice What `reclaim(childId)` would return now; zero if nothing is reclaimable.
/// MUST NOT revert.
function reclaimableOf(uint256 childId) external view returns (uint256);
/// @notice Return a dead child's unallocated share to its parent. Idempotent.
function reclaim(uint256 childId) external returns (uint256 amount);
}What this profile does and does not bound. Debiting the immediate parent bounds the breadth of a lineage: a parent cannot commit more than it holds, however many children it spawns. It does not bound depth. A child debited from its parent still begins with its own allocation counter at zero, so a chain of D generations each re-issuing its full cap reaches D times the root's ceiling. An implementation that needs a true aggregate bound MUST debit every ancestor along the lineage at spawn, or keep equivalent lineage-wide accounting. This profile specifies the per-parent partition and its reclamation; it does not claim the stronger property.
Interface detection
A conforming contract MUST implement ERC-165 and MUST return true for the interface id of
IInheritableAgentMandate. An implementation adopting the partitioned budget profile MUST
additionally return true for the interface id of IInheritableAgentMandatePartitioned.
Rationale
Why weld the mandate into identity rather than store it in a mutable side record. A mutable record can be loosened by whoever controls the write path β including the agent. Committing the mandate into the identity makes "loosen my own limits" and "become a different, unrecognised agent" the same act, which is the property the whole scheme rests on.
Why child β parent on every clause, not only on spend. Aggregate-spend inheritance (as in
the Bounded Agent Actions proposal's delegation-budget profile) bounds what a tree may collectively spend but leaves the
other clauses β payees, expiry, lease, freeze, reproduction β free to reset at each child. Those
are precisely the clauses that determine what a child is allowed to be. Narrowing all of them
together is what makes a subtree strictly less capable than its root.
Why a telomere. A pure spend cap does not bound reproduction depth. A monotonically
decreasing generation counter bounds the tree's depth without any global registry of siblings,
keeping spawn a local, cheap operation.
Why the effective cap is a lineage minimum, not the local value. It makes an ancestor's freeze or expiry bind every descendant automatically, so control applied high in the tree cannot be outrun by spawning deeper.
Why soulbound. Measured against the deployed ERC-8004 Identity Registry rather than
assumed. Transferring an agent clears only the reserved agentWallet key; arbitrary metadata β
including a mandate written there β survives the transfer intact. The risk is therefore not that
the clauses are detached, but that they persist while becoming rewritable by whoever now holds
the token: acting as the new owner, a single setMetadata call reset a declared spend cap from
1000 to 999,999,999 and a generation counter from 3 to 255, with no refusal. A leash whose
setting is controlled by the holder is not a leash. Non-transferability, together with the
absence of any post-hoc mutation of the clauses, is therefore load-bearing rather than
incidental β at the cost of losing the transferability benefits of ERC-721, which this standard
deliberately trades away.
Why guardianship is a role, not the agent. The exception to a hard rule (renewing life,
freezing) must come from outside the bound party, or the bound party can un-bind itself. Keeping
mint/freeze/renew guardian-only is the on-chain expression of "the agent cannot authorise
itself."
Why on-chain rather than off-chain policy. It makes the constraints verifiable and portable across wallets and agent frameworks, which is the property no single wallet-vendor policy engine provides.
Why partitioning is optional but reclamation inside it is not. An implementation that needs only a per-agent ceiling should not have to carry allocation state, so the partition is a profile rather than the baseline. But an implementation that does partition has created a committed-and-unspendable quantity, and declining to return it is not a simpler design β it is an accounting that never closes. The choice is whether to partition; once made, the credit side follows from the debit side.
Why death is local and inactivity is not. isActive deliberately walks the lineage, so a
descendant of a frozen ancestor reads inactive. Reclamation cannot use that predicate: freezing
is reversible and reclamation is not, so a temporary state high in the tree would permanently
dissolve allocations below it. Keeping the two predicates distinct is what makes a freeze a
pause rather than a confiscation.
What this deliberately does not do. It does not bound aggregate spend across siblings (see Security Considerations), it does not verify off-chain behaviour, and it does not replace the metering standards it composes with; it adds the identity-and-inheritance layer they leave open.
Backwards Compatibility
This standard introduces new interfaces and does not modify existing ones. It composes with
ERC-8004 (identity), and is designed to sit alongside ERC-8226 (mandate clauses), the Bounded
Agent Actions proposal (bounded/aggregate metering), and ERC-7710 (delegation)
rather than replace them. agentId MAY be an ERC-8004 agentId. The mandate clauses are
intentionally aligned with ERC-8226 so an ERC-8226 enforcer can consume
effectiveMaxSpendWei/isActive. Delegations under ERC-7710 MAY be scoped to an agentId and
checked against isActive.
The one deliberate incompatibility is with ERC-721 transferability: identities under this standard are soulbound and MUST revert on transfer. Implementations that expose an ERC-721 surface for tooling compatibility therefore conform to ERC-721's interface but intentionally violate its transfer semantics; this is stated explicitly so integrators do not rely on transfer.
Test Cases
The following behaviours are normative and MUST hold. They correspond to the escape-refusal tests in the reference implementation, which run outside any AI model.
- Widen-cap refusal.
spawnwithchildMandate.maxSpendWei > parent.maxSpendWeiMUST revert. - Extend-expiry refusal.
spawnwithchildMandate.validUntil > parent.validUntilMUST revert. - Drop-expiry refusal. With
parent.validUntil != 0,spawnwithchildMandate.validUntil == 0MUST revert. - Already-expired refusal.
spawnwith a non-zerochildMandate.validUntilalready in the past MUST revert. - Weaken-lease refusal. With
parent.requireLease == true,spawnwithchildMandate.requireLease == falseMUST revert. - Telomere floor.
spawnfrom a parent withtelomere == 0MUST revert; a successfulspawnyieldschild.telomere == parent.telomere - 1; no call raisestelomere. - Payee subset.
spawnproposing a payee not contained in the parent's set MUST revert. - Zero-owner refusal.
spawnwithchildOwner == address(0)MUST revert. - Effective cap. For a chain
A(100) β B(80) β C(90),effectiveMaxSpendWei(C) == 80. - Freeze cascade. After
freeze(A),isActive(B)andisActive(C)returnfalsewith no per-node call. - Self-authorisation refusal. A call to
mint/freeze/renewfrom a non-guardian (including the agent itself) MUST revert. - Unknown id.
isActiveon an id that was never minted MUST returnfalse, nottrue. - Non-transferability. Every transfer entry point MUST revert.
The following apply only to an implementation adopting the partitioned budget profile.
- Over-commitment refusal.
spawnwithchildMandate.maxSpendWei > availableBudget(parentId)MUST revert. - Debit on spawn. A successful
spawnincreasesallocated(parentId)by exactlychildMandate.maxSpendWei. - Reclamation on a frozen child. After the guardian freezes a child,
reclaim(child)returns that child's unallocated remainder and restoresavailableBudget(parent)by that amount. - Reclamation on an expired child. A child that passes its own deadline with no intervention becomes reclaimable; the death condition is reached without any act by the child or its owner.
- Live child under a frozen ancestor. With the parent frozen and the child itself neither
frozen nor expired,
reclaim(child)MUST revert.isDead(child)isfalseeven thoughisActive(child)isfalse. - Idempotence. A second
reclaimon the same child MUST NOT credit the parent again. - No double counting. Reclaiming a child returns only its own unallocated remainder; amounts the child committed to grandchildren are unaffected.
- Genesis refusal.
reclaimon an identity with no parent MUST revert. - Authorisation.
reclaimfrom any caller other than the parent's owner or the guardian MUST revert. - Clauses untouched. The identity commitment of both child and parent is unchanged by a reclamation.
Reference Implementation
A reference contract demonstrates the behaviors specified here β the inheritance predicate
(child β parent on every clause), the telomere, the cascading freeze, soulbound identity, the
validUntil expiry, and a mandateRoot that binds every clause into the identity hash (so editing
any clause changes the identity). It is deployed on the Base Sepolia testnet (chainId 84532) at
0x344cda78e7208684edf9a6241f5b95b1698e576a, where the cascade (freeze the genesis and a
depthβ16 descendant reads inactive), the validUntil monotonicity refusals (a child expiring later
than its parent, or removing an inherited expiry, both revert on-chain), and non-strippability (two
mandates differing only in validUntil yield distinct mandateRoots) are exercised in on-chain
transactions with reproducible cast calls. An earlier version without the expiry clause remains
live at 0x2d463db56fadb55cd451d2c3237ec2213ba3bda9.
The reference implements a subset of the interface above: mint, spawn, freeze, isActive,
mandateRoot, mandateOf, parentOf and ownerOf, with an enumerated payee allowlist.
renew, effectiveMaxSpendWei and verifyDescendant are specified here and are not present in
that deployment.
A second reference implements the partitioned budget profile, deployed on Base Sepolia at
0x299ee791afed7d89548dc9133af8387701617f70. Three scenarios were exercised on-chain rather
than only in local tests:
- a child frozen by the guardian, then reclaimed β succeeded, returning the child's unallocated remainder and restoring the parent's available budget;
- a child left to pass its own deadline with no intervention β reclamation succeeded 68 seconds of real clock later, with no act by the child or its owner, which is the death condition requirement met in practice rather than asserted;
- a live child under a frozen parent β reverted with
child not dead. The parent was frozen, soisActive(child)was false, yet the child was neither frozen nor expired and its share stayed committed. This is the locality requirement above, measured.
The remaining profile behaviours β idempotence, the genesis refusal, the authorisation rules, and the absence of double counting across three generations β are covered by the local test suite and are not separately exercised on-chain.
Two companion cursors (MandateAwareCursor, MandateAwareAggregateCursor) demonstrate composition
with the Bounded Agent Actions proposal's flat and aggregate frozen profiles and pass its conformance suite (9 of 9) under
local testing; they are exercised in-repo and are not themselves deployed on-chain.
One measured note carried up from the reference deployment: isActive walks the ancestor chain
unbounded, costing ~4,454 gas per generation, so a lineage deep enough (thousands of generations)
makes the read exceed practical eth_call limits β a liveness/read cost, not a way to bypass a
freeze. A conforming deployment SHOULD bound the genesis generation count well below the telomere
type's maximum so that isActive stays callable. Reference material is illustrative and unaudited;
see Security Considerations.
Security Considerations
The escape hatch is real and this standard does not close it. On-chain mandates bind only agents whose actions route through compliant infrastructure. An agent capable of writing its own code, deploying to infrastructure it controls, and funding itself can reproduce onto a chain or runtime this standard does not govern. It does not break the rule; it walks away from it. No construction in the identity stops this β only the surrounding ecology (what infrastructure and funding the agent can reach, whether honest compute dominates, whether anyone is watching), and only at a cost.
Metering is not enforcement; closing that gap is a substrate obligation. This standard
records and gates against effectiveMaxSpendWei and isActive, but a contract that only reads
these values does not physically stop an agent that moves value by a path that never consults
them. Closing this requires a substrate that makes the metered path the only path: either
custody (the substrate holds the assets, so spending must go through the checked path) or an
execution gate (a smart account, e.g. ERC-4337/ERC-7579, whose sole execution path checks the mandate).
Which is preferable is left to the substrate and is an open design question.
A compromised guardian is total control. The guardian can mint, freeze and renew leases; a compromised guardian key can freeze arbitrarily or keep an agent alive past intended limits. Guardians SHOULD be threshold/multisig sets with proof-of-personhood where possible, never a single hot key. This standard reduces but does not eliminate guardian trust.
The per-child ceiling does not bound aggregate sibling spend. Ten children under a parent capped at N are each capped at N (or lower), so their branch combined may exceed N. Deployments that need an aggregate bound SHOULD partition the budget at spawn (debiting the parent) rather than only enforcing a per-child ceiling. Debiting the immediate parent alone is not sufficient: it bounds breadth, not depth. A child that has been debited from its parent still begins with its own allocation counter at zero, so a chain of D generations each re-issuing its full cap reaches D times the root's ceiling β depth substitutes for width. An aggregate bound therefore requires debiting every ancestor along the lineage at spawn, or an equivalent lineage-wide accounting; a per-parent counter alone does not provide one. The per-parent partition, and the reclamation it obliges, are specified as an optional profile rather than as the baseline, because they add on-chain state that an implementation needing only a per-agent ceiling should not have to carry.
A spawned child with no owner is unreclaimable, and unclosable. spawn MUST reject a zero
child owner. A child spawned to the zero address is debited against its lineage like any other,
but no party can act for it. Under any death condition the child or its owner must reach, that
slice never becomes reclaimable β it does not merely sit unused, it sits outside any
conservation rule, which then has no way to close for that lineage. The general requirement is
not the zero check: reclamation needs a death condition reachable without the child's
cooperation. A time-based trigger satisfies it; a cooperative one does not. Conservation that
depends on the constrained party cooperating is not conservation.
Unknown agent ids read as active unless rejected. A registry backed by mappings returns a
zero-valued Mandate for an id that was never minted: frozen is false, validUntil is zero,
and the ancestor walk terminates immediately. A naive isActive therefore reports an agent that
does not exist as active, and a capability root can be computed for it. Implementations MUST
reject unknown ids in isActive and in any root-deriving view, and integrators MUST NOT treat
isActive as an existence check.
A capability root is not a liveness attestation. A root derived from an agent's own clauses
does not change when an ancestor is frozen, when the agent's unallocated budget reaches zero, or
when a payee is removed β none of that is part of the agent's own Mandate. Consumers that pin
a root MUST re-read isActive and the effective cap at use time rather than treating the root
as a standing authorization.
Spawning is not gated on liveness unless required. An implementation that checks only that
the parent is unfrozen β not its expiry, nor the freeze state of its ancestors β lets a dead
lineage keep growing. The descendants are inert (isActive walks up), so nothing becomes
spendable, but identities and any partitioned budget are consumed irreversibly where the
allocation counter never decreases. Implementations SHOULD require isActive(parentId) in
spawn.
Unbounded population. Nothing bounds the number of descendants: a parent with no
unallocated budget left can still spawn arbitrarily many zero-budget children, and each is inert
(effectiveMaxSpendWei takes the lineage minimum, so a zero cap dominates). Where the deployment
partitions the budget at spawn, this is harmless β no view is O(width), so the spawner merely
pays gas for empty shells. Without that partitioning, unbounded width is the vector described
above. It stops being harmless in either case the moment anything grants value per agent:
deployments layering airdrops, reputation weight, quotas, or rate limits on top of agent identity
reintroduce a Sybil vector this ERC does not cover, and SHOULD bound population explicitly.
No custody. This standard authorizes; it does not hold funds. A conforming registry needs no payable entry point and never takes possession of value, so expiry cannot strand a balance and deactivation leaves no orphaned funds. Implementers who add a custody layer inherit the question this standard avoids: an expired or frozen agent MUST be able to unwind β settle outstanding obligations and return residual value up its lineage β and not merely lose the ability to act, or expiry becomes a way to lock value permanently.
Liveness griefing. A guardian that stops renewing deactivates a subtree by design
(dead-man's switch). Implementers SHOULD choose validUntil windows that tolerate renewal
latency.
Payee revocation. With a payeesRoot, revoking a payee requires updating the root;
implementations SHOULD emit an event on root changes.
Reentrancy and gas. spawn MUST write state after all checks. The ancestor-walking views
(isActive, effectiveMaxSpendWei) are O(depth), and callers SHOULD bound lineage depth.
Soulbound enforcement is only as strong as the identity substrate. Guaranteeing non-transferability ecosystem-wide may require identity-standard-level support; an implementation that layers over a transferable identity token can only enforce non-transfer within its own surface.
This governs money and identity, not thought. It is not an alignment technique and does nothing about a model that lies, hallucinates, schemes, or misreads intent.
Trust must still begin somewhere. The genesis mandate is minted by a first guardian; the chain does not remove that first act of trust, it only makes it public, permanent, and bound by the inheritance rule thereafter. A founder-key genesis is acceptable only with a credible plan to hand the guardian role to a multisig or DAO.
Copyright
Copyright and related rights waived via CC0.
