Originally published on Ethereum Magicians.
Standards look cleanest before somebody integrates them.
ERC-8226, the Regulated Agent Mandate standard, defines a way for a principal to authorize an agent to act against a regulated asset subject to explicit permissions, limits, expiry, revocation, and compliance checks.
On paper, the model is straightforward.
A principal grants a mandate. An agent presents it. The registry evaluates whether the action is allowed. The asset executes or rejects it.
We wanted to know what that actually felt like from the asset side.
So rather than review the interface in isolation, we implemented it.
We Started From the Published Standard, Not the Reference Code
We built our token-side integration from the published ERC-8226 specification without coordinating with the authors first.
The goal was deliberate: approach the standard the way an outside protocol team would.
The integration went live on Ethereum Sepolia against the reference AgentMandate deployment.
We deployed three components.
GatedUSDRams is a RAMS-aware ERC-20 that gates agent-initiated transfers through ERC-8226.
VARComplianceProviderAdapter implements the compliance-provider interface used by the mandate registry.
DelegationMirror is the delegation registry the adapter reads when determining whether the principal and agent relationship is valid.
The point was not to build another demo.
The point was to see whether an unrelated asset could consume the standard, execute through it, fail through it, and explain those failures without private knowledge of the reference implementation.
That last requirement turned out to matter.
The Happy Path Worked
We granted a mandate through the reference AgentMandate contract with our adapter configured as its compliance provider.
An agent then initiated a transferFrom through our token.
It cleared.
The registry recorded the execution and increased the mandate's cumulative usage.
We then deliberately attempted another transfer that exceeded the transaction cap.
That one failed.
The important part was everything around the failure.
The blocked transaction did not alter the destination balance. The mandate's cumulative usage did not advance. The state therefore behaved atomically: either the regulated action executed and was recorded, or neither happened.
That is what we wanted to see.
We also reconstructed the reference implementation using the deployment toolchain and compared the resulting runtime bytecode with the contracts actually deployed to Sepolia.
Apart from the metadata tail, they matched.
So the behavior we were exercising was not an approximation of the reference deployment. It was the reference behavior.
And importantly, we found no contract vulnerability in that implementation.
The interesting findings were at the integration boundary.
false Is Not Enough Information for an Agent
ERC-8226's canExecute decision ultimately collapses multiple failure conditions into a boolean.
Allowed or not allowed.
That sounds reasonable until the caller is autonomous software.
Consider what false can mean.
The mandate may have expired.
The principal's compliance status may no longer be valid.
The requested transaction may exceed a per-transaction cap.
The mandate may have exhausted a cumulative allowance.
The mandate may have been revoked.
The caller may be using the wrong asset.
There may be no mandate at all.
These conditions require completely different responses.
An agent faced with an exceeded transaction cap might reduce the amount and retry.
An expired mandate requires a new authorization.
A revoked mandate should normally stop.
A temporary compliance condition may require waiting.
A malformed call is an implementation bug.
Returning the same boolean for all of them forces the integrator to reconstruct information the registry already possesses.
That is exactly what we ended up doing.
We Had to Reimplement the Registry's Decision Tree
Our token exposes a diagnostic path that walks through the registry checks and determines which condition failed.
That let the blocked transfer return a reason equivalent to RAMS_OVER_TX_CAP.
It worked.
It is also the wrong abstraction.
To produce that diagnosis, the integration has to retrieve the mandate and reproduce the registry's validation order. On a revert path that can require several additional external reads.
Worse, the diagnostic implementation is coupled to the registry's internal control flow.
If the registry changes its validation order, an external diagnostic function can silently become wrong while still compiling and still returning plausible answers.
An integrator should not need to mirror the internals of an authorization system to explain why authorization failed.
The better interface is for the authorization decision itself to return both pieces of information:
canExecute(...) -> (allowed, reason)
One implementation of the checks.
One authoritative ordering.
One result for integrators.
For autonomous agents, this distinction is especially important. A reason code is not merely better UX. It is machine-readable recovery policy.
Compliance Expiry Exposed a Second Gap
The compliance-provider interface can return an expiration time when evaluating a principal.
But that expiration is not retained in the mandate itself.
That creates a subtle distinction between authorization time and execution time.
A principal can satisfy a KYC or eligibility check when the mandate is created. If that eligibility expires later, the mandate needs some mechanism for recognizing that fact.
Our integration chose the strict approach: re-evaluate the principal on execution.
That closes the window for our asset because the latest compliance state is consulted before the action executes.
But the standard can make the invariant stronger.
If the compliance provider already publishes an expiresAt, the mandate can store it and include it in authorization evaluation.
That gives the mandate an explicit upper bound tied to the compliance state that permitted its creation.
Live re-checks can still be used by assets requiring stronger guarantees.
The two approaches are complementary.
Revocation Has to Preserve History
The most dangerous edge case we found was not a sophisticated exploit.
It was an implementation choice that looks like a perfectly reasonable gas optimization.
When a mandate is revoked, the reference implementation marks it revoked rather than deleting its record.
That matters.
Some integration patterns determine whether RAMS rules apply by checking whether a mandate record exists. If an implementation deletes the record entirely, downstream code can interpret the missing mandate as "no RAMS relationship exists" and fall back to ordinary token allowance behavior.
An agent that still possesses an ERC-20 allowance can therefore become less restricted after revocation than it was before.
That is the opposite of what revocation is supposed to mean.
The reference implementation avoids the problem because the record persists.
But reference behavior and normative specification are not the same thing.
If safety depends on persistence, persistence should be part of the standard.
The mandate record should not be deleted when revoked.
A revoked authorization is still part of the security state.
There Is No Global Agent Budget
ERC-8226 limits an agent within an individual principal's mandate.
That means an agent holding mandates from ten principals effectively has ten independent budgets.
There is no protocol-level aggregate ceiling across those principals.
We do not consider that a defect by itself.
It may be exactly the right boundary.
Each principal controls their own exposure. A global agent cap introduces a different risk model and potentially a different authority model.
But it is the kind of property integrators otherwise have to derive themselves.
Standards benefit from stating these boundaries explicitly, especially when they concern risk.
We Found a Bug in Our Own Code Too
Integration reviews should be symmetric.
It is easy to treat the reference implementation as the object under inspection and your own code as the measuring instrument.
The measuring instrument can be wrong.
During this work we found a revocation bug in our own registry design.
Our revoke() path left the mandate nonce unchanged. Under the wrong sequence of operations, a dead mandate could be rebound and the revocation latch reset.
We fixed it and added regression coverage.
The underlying failure mode was familiar: one piece of state advanced while another piece of state that defined the same authorization lifecycle did not.
That is exactly why exercising a protocol through complete state transitions is more valuable than merely checking whether each function behaves sensibly in isolation.
The Integration Has Limits
Our own implementation should not be described as stronger than it is.
The compliance adapter uses an attested-personhood model rather than a trustless cross-chain World ID verification path.
The attestor is a single externally owned account rather than a multisig.
Some compliance-provider methods deliberately revert because they are not part of the operating model we implemented.
And the integration has not been audited.
Those constraints do not invalidate the ERC-8226 findings.
They define the security boundary of the system that produced them.
That distinction matters.
What We Would Change in ERC-8226
The implementation exercise produced a fairly small set of specification changes.
canExecute should return a structured reason alongside the boolean decision.
The specification should define deterministic precedence when more than one validation condition fails.
Principal compliance expiry should be representable in the mandate and enforced during execution.
Revocation should preserve the mandate record rather than deleting authorization history.
And the standard would benefit from a documented agent-custodied example, because autonomous payments frequently involve an agent controlling funds directly rather than merely initiating a transfer from a principal-controlled wallet.
None of those changes require reinventing the model.
That is a good sign.
The basic architecture held up.
The issues appeared where standards usually become real: error semantics, lifecycle state, and the assumptions one implementation makes about another.
Standards Are Interfaces Between Teams
The most useful result of this exercise was not finding something catastrophic.
It was finding the things an independent integrator has to guess.
A protocol standard succeeds when two teams that have never spoken can implement opposite sides of an interface and arrive at the same behavior.
That includes failure behavior.
It includes revocation.
It includes expiry.
And, increasingly, it includes enough structured information for autonomous software to decide what to do next.
ERC-8226 is still a draft.
That is exactly when integration friction is useful.
The right time to discover that a boolean does not carry enough information is before dozens of assets independently build their own diagnostic layer around it.