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:

  1. setIdentityRegistry(address) — swap the configured ERC-8004 registry
  2. upgradeToAndCall(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 agentId is 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 _bindings mapping 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 setAgentURI will 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.
  • initialize on the proxy is gated by the initializer modifier and cannot be called twice.
  • initialize rejects a zero identityRegistry_ and a zero initialOwner.

If you see an InvalidInitialization() revert during deployment, you're probably calling initialize on the implementation instead of through the proxy.