Admin & Upgrades
What the adapter owner can do, what they can't, and how to reason about the upgrade trust model.
The admin role#
The adapter uses OpenZeppelin's OwnableUpgradeable. A single owner
address has two privileged powers:
setIdentityRegistry(address)— swap the configured ERC-8004 registryupgradeToAndCall(address, bytes)— upgrade the implementation (UUPS)
Everything else is open to whoever currently controls the bound token.
What the admin cannot do#
- Steal, reassign, or delete individual bindings
- Bypass the controller check on any write
- Change which external token a given
agentIdis bound to - Mint ERC-8004 identities that were not requested by a token controller
The binding model and controller checks are enforced by the implementation's bytecode, not by the admin. The admin's power is limited to replacing that implementation — which is a trust assumption you can opt out of.
Upgrade trust model#
UUPS upgrades let you deploy a new implementation and point the proxy at it with a single owner-only call. This is powerful and dangerous in equal measure:
- Safe use: tracking future ERC-8004 registry changes, patching bugs, improving gas.
- Unsafe use: a rogue owner could upgrade to a malicious implementation that bypasses controller checks.
If your threat model does not tolerate a trusted owner, renounce ownership
after initial deployment and any intended upgrades. Calling renounceOwnership()
transfers ownership to address(0), which permanently locks both
upgradeToAndCall and setIdentityRegistry. The tradeoff is that you can no
longer migrate to future ERC-8004 registry versions — any protocol upgrade
that requires swapping the registry address will leave your adapter frozen.
Registry swaps#
setIdentityRegistry(newRegistry) updates the storage slot the adapter
forwards writes into. It does not migrate any existing data:
- The adapter's local
_bindingsmapping is unchanged. - The old ERC-8004 registry still holds the ERC-8004 tokens the adapter registered through it.
- Controller-gated writes on old agentIds will now be routed to the new
registry, which has no record of those ids, so calls like
setAgentURIwill revert at the new registry's internal authorization step.
Registry swaps are therefore only safe when:
- The new registry has been populated with the same agentIds (by some external migration process), or
- You are accepting that old bindings become read-only artifacts in the adapter's local storage.
Document the intended scenario clearly before flipping the switch on a live deployment.
Storage layout and future upgrades#
The current storage layout is:
slot 0 : identityRegistry (address)
slot 1+ : _bindings (mapping)
slot N+ : _agentIdByBinding (mapping)
plus whatever slots OwnableUpgradeable, Initializable, and
UUPSUpgradeable reserve. Any future upgrade must preserve the order of
these variables. The current implementation does not include a storage gap,
so appending new state variables is only safe if they go after the
existing ones — which is the standard Solidity rule.
If you plan to add significant state in a future version, consider adding
a __gap array in a maintenance upgrade before you need it.
Initialization safety#
- The implementation constructor calls
_disableInitializers()so the bare implementation contract cannot be initialized directly. initializeon the proxy is gated by theinitializermodifier and cannot be called twice.initializerejects a zeroidentityRegistry_and a zeroinitialOwner.
If you see an InvalidInitialization() revert during deployment, you're
probably calling initialize on the implementation instead of through the
proxy.