The gap
AddressRegistry.register takes effect in the block it lands in, and
LibAddressRegistry.resolve returns whatever root has bound at the moment it
is called. The threat model in IAddressRegistryV1 accepts that and argues it
is contained:
A compromised root therefore cannot touch anything deployed. It can point a
name at an address it controls, so that a deployment made after the
compromise snapshots that address. That is caught by verifying a deployment
after deploying it and before anything depends on it… A poisoned deploy
is a burned deterministic address, discovered before use.
That containment rests on the resolve happening at a moment someone is
watching — a deploy, which is followed by verification. It does not hold for a
resolve that happens at an arbitrary later moment.
Why that matters in practice
A consumer behind a beacon has two one-shot entry points, not one: the
initializer that runs inside a new proxy's construction, and a
reinitializer(N) that reconciles an existing proxy after a beacon
upgrade. The second is the hazard. An upgrade that re-reads a name is a
routine governance action, months or years after the implementation was
audited and its creation code pinned, and it is the one moment nobody
re-audits a registry binding.
So a root compromise need not be noticed when it happens. It can sit dormant,
bound to the honest address, and be switched immediately before a consumer's
admin-reconciling upgrade. The compromise converts to live admin capture at a
moment the victim chose, which is also the moment the victim is least
likely to be looking for it. There is no burned-address signal, because
nothing new is deployed.
The registry's own guidance points the same way — "it must not re-read at the
point of use" — but that is a rule consumers can follow or not, and a
consumer following it exactly still has no defence if the one read it does
make happens to fall after a switch.
What would close it
A delay between register and the binding being resolvable: register
records a pending binding and a timestamp, get keeps answering the previous
one until the delay elapses. A rebind then has to be public for the delay
window before it can affect any resolve, so a consumer's post-deploy
verification and the org's own monitoring both get a chance to see it, and a
switch timed to an upgrade is no longer possible.
This is a change to the registry's shape, which the current NatSpec forbids on
the existing contract — "There is no removal, no upgrade and no authority
beyond root, and an implementation MUST NOT add any" — so it is a new
AddressRegistry version at a new deterministic address, with the existing
one left where it is. Raising it rather than proposing a patch, because the
delay length, and whether a pending binding is cancellable and by whom, are
org decisions.
Worth noting the migration cost is already live: bindings do not carry between
registry versions, and the pinned address has already moved once
(0x8cACfbD5d78b6D87080cE0839708ac2dA5461F78 at 0.1.7/0.1.8 →
0xDBb7Cf1cba967d2D577045E138eEd359675a6fa7 at 0.1.10), so a consumer's
resolved value is a function of the rain-deploy version it pins.
Raised while adopting the registry in S01-Issuer/st0x.deploy#392, where the
conclusion was to keep the upgrade-path reconcile reading msg.sender under
onlyRole(DEFAULT_ADMIN_ROLE) rather than resolving a name, precisely to stay
out of this.
🤖 Generated with Claude Code
The gap
AddressRegistry.registertakes effect in the block it lands in, andLibAddressRegistry.resolvereturns whatever root has bound at the moment itis called. The threat model in
IAddressRegistryV1accepts that and argues itis contained:
That containment rests on the resolve happening at a moment someone is
watching — a deploy, which is followed by verification. It does not hold for a
resolve that happens at an arbitrary later moment.
Why that matters in practice
A consumer behind a beacon has two one-shot entry points, not one: the
initializer that runs inside a new proxy's construction, and a
reinitializer(N)that reconciles an existing proxy after a beaconupgrade. The second is the hazard. An upgrade that re-reads a name is a
routine governance action, months or years after the implementation was
audited and its creation code pinned, and it is the one moment nobody
re-audits a registry binding.
So a root compromise need not be noticed when it happens. It can sit dormant,
bound to the honest address, and be switched immediately before a consumer's
admin-reconciling upgrade. The compromise converts to live admin capture at a
moment the victim chose, which is also the moment the victim is least
likely to be looking for it. There is no burned-address signal, because
nothing new is deployed.
The registry's own guidance points the same way — "it must not re-read at the
point of use" — but that is a rule consumers can follow or not, and a
consumer following it exactly still has no defence if the one read it does
make happens to fall after a switch.
What would close it
A delay between
registerand the binding being resolvable:registerrecords a pending binding and a timestamp,
getkeeps answering the previousone until the delay elapses. A rebind then has to be public for the delay
window before it can affect any resolve, so a consumer's post-deploy
verification and the org's own monitoring both get a chance to see it, and a
switch timed to an upgrade is no longer possible.
This is a change to the registry's shape, which the current NatSpec forbids on
the existing contract — "There is no removal, no upgrade and no authority
beyond root, and an implementation MUST NOT add any" — so it is a new
AddressRegistryversion at a new deterministic address, with the existingone left where it is. Raising it rather than proposing a patch, because the
delay length, and whether a pending binding is cancellable and by whom, are
org decisions.
Worth noting the migration cost is already live: bindings do not carry between
registry versions, and the pinned address has already moved once
(
0x8cACfbD5d78b6D87080cE0839708ac2dA5461F78at 0.1.7/0.1.8 →0xDBb7Cf1cba967d2D577045E138eEd359675a6fa7at 0.1.10), so a consumer'sresolved value is a function of the rain-deploy version it pins.
Raised while adopting the registry in
S01-Issuer/st0x.deploy#392, where theconclusion was to keep the upgrade-path reconcile reading
msg.senderunderonlyRole(DEFAULT_ADMIN_ROLE)rather than resolving a name, precisely to stayout of this.
🤖 Generated with Claude Code