feat: Versioned Registry Router + Upgradeable Proxies for All Core Contracts#310
Open
0xCardiE wants to merge 7 commits into
Open
feat: Versioned Registry Router + Upgradeable Proxies for All Core Contracts#3100xCardiE wants to merge 7 commits into
0xCardiE wants to merge 7 commits into
Conversation
- Wire core contracts and local deploy to the registry router - Add registry deploy scripts, tests, and spec for the router - Mine stats witnesses in tests; remove checked-in fixture JSON
Convert all core contract deployments (PostageStamp, PriceOracle, StakeRegistry, Redistribution) from direct deploys to TransparentUpgradeableProxy + DefaultProxyAdmin + initialize() across local, main, test, and tenderly networks. Add VersionedRegistryRouter deploy script to all networks that registers all 4 proxies and their v1.0.0 releases with codehash verification. Update role scripts to use keccak256 hashes instead of read() calls to avoid TransparentUpgradeableProxy admin-cannot-fallback errors. Remove superseded deploy/registry/ directory.
Security and correctness fixes for the new proxy registry / router: - Gate forwardUnchecked behind ROUTER_ADMIN_ROLE so it no longer bypasses the selector allowlist for arbitrary callers. - Reject zero codehash in registerRelease so verifyProxy can't be silently disabled. - Validate registered proxies are actually owned by this router's ProxyAdmin. - Add deprecateProxy + ProxyDeprecated event; verifyProxy now rejects deprecated proxies and verifyAllProxies skips them instead of reverting. - Reject calldata < 4 bytes in both forward paths instead of panicking. - Add explicit ZeroAddress check on the constructor's _proxyAdmin arg and a SelectorRouted event for setRoutedSelector. Multisig handover (deploy/main/012): - Grant REGISTRAR_ROLE / DEPRECATOR_ROLE / ROUTER_ADMIN_ROLE to the multisig and renounce them (and DEFAULT_ADMIN_ROLE) from the deployer EOA so the multisig is the sole authority on the router. Tooling and dead-code cleanup: - Restore working solhint config (extends solhint:recommended; the removed solhint:default preset broke the linter). - Drop unused imports/vars and dead scaffolding in scripts/mine-stats-witnesses.ts and deploy/local/011. - Prettier-format remaining files touched on this branch. - Expand VersionedRegistryRouter tests to cover the new behaviour (proxy admin mismatch, deprecateProxy, calldata length, zero codehash rejection, forwardUnchecked role gating, selector disable). All 225 tests pass.
The .ts source was formatted in dad1e86 but the compiled .js sibling (loaded by worker_threads at runtime) was missed; CI's prettier --check runs against both.
It's the CommonJS sibling of mine-worker.ts, loaded directly by worker_threads at runtime, so the require() calls flagged by @typescript-eslint/no-var-requires are intentional and can't be rewritten as ESM imports without breaking the worker.
…plementation Add a new transparent proxy that runs `VersionedRegistryRouter.verifyProxy` in `_beforeFallback`, so every non-admin call atomically rejects unregistered, deprecated, or codehash-mismatched implementations without changing core contract code. Add `pinnedExecute(address expectedImpl, bytes data)` so callers (e.g. Bee nodes) can pin the implementation address derived from their trusted versionId; reverts with `PinMismatch` if the proxy is later upgraded behind their back, and rejects admin to keep the transparent-proxy property. Expose `verifyImplementation(address)` on the router for the per-address checks (registration, deprecation, codehash) so clients can verify without needing a registered proxyId. Tests cover registry-side `verifyImplementation`, guarded-proxy delegation and revert paths, and the new `pinnedExecute` happy path, mismatch, registry failure, admin rejection, and bubbled implementation revert.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces a versioned registry + router for Swarm storage-incentive contracts deployed behind upgradeable proxies. The registry publishes immutable
version → implementationmappings (address + codehash + semver + deprecation). Clients (e.g. Bee) must not blindly trust “whatever sits behind this proxy today”; they pin a trusted version and verify chain state before (or during) protocol calls.New since the original PR text:
verifyImplementation(address implementation)onVersionedRegistryRouter— same implementation-level checks as insideverifyProxy, without requiring a registeredproxyId(useful for tooling and pure reads).RegistryGuardedTransparentUpgradeableProxy— extends OZTransparentUpgradeableProxy: on every non-adminfallback/receive, runsverifyProxy(registryProxyId)then delegates (msg.senderunchanged ).pinnedExecute(address expectedImpl, bytes calldata data)on that guarded proxy — atomic path for Bee: verifies registry + requiresaddress(this)’s current implementation equal toexpectedImpl, then **delegatecall**s into the live implementation withdata. If an operator upgrades the proxy to another registered build, a node still pinned to the old implementation reverts in the same transaction (PinMismatch).Architecture (high level)
VersionedRegistryRouter.forwardremains an optional path: selector-gatedverifyProxy+callto the proxy. Implementations invoked viaforwardseemsg.sender == router, so it is not a universal replacement for direct node calls.On-chain API changes (VersionedRegistryRouter)
All view unless noted.
registerRelease(versionId, semver, implementation, codehash)deprecateRelease(versionId)getRelease(versionId)ReleaseInfo:implementation,exists,deprecated,codehash,semver.getVersionForImplementation(implementation)bytes32version id or zero).verifyImplementation(implementation)extcodehash(implementation)matches storedcodehash. ReturnsversionIdon success.registerProxy(proxyId, proxy)proxymust use the sameProxyAdmininstance the router was constructed with.deprecateProxy(proxyId)verifyProxy/ guarded fallback revert for this proxy id.getProxyImplementation(proxyId)verifyProxy(proxyId)verifyImplementationrules. Returnsimplementationon success.verifyAllProxies()forward(proxyId, data)/forwardUnchecked(proxyId, data)msg.sendercaveat above.On-chain API — RegistryGuardedTransparentUpgradeableProxy
Deploy args (in addition to OZ transparent proxy):
VersionedRegistryRouter registry,bytes32 registryProxyId(must matchregisterProxy).fallback)_beforeFallback:verifyProxy(registryProxyId)→ delegate to_implementation().pinnedExecute(expectedImpl, data)verifyProxy→require(_implementation() == expectedImpl)→delegatecall(implementation, data).msg.senderin the implementation is the EOA / contract that calledpinnedExecute. Payable; ETH forwards to implementation via delegate context rules.upgradeTo, etc.)ifAdminpath — does not runpinnedExecute/ pin checks on those admin functions.pinnedExecutereverts formsg.sender == proxy admin(AdminCannotPin).Selector / collision note:
pinnedExecuteusesbytes4(keccak256("pinnedExecute(address,bytes)")). Core implementations must not declare a public/external function with the same selector on the same proxy, or calls could be ambiguous (always route Bee traffic throughpinnedExecuteexplicitly in the client).What Bee nodes should do
1. Configuration (local, trusted)
registryRoutercontract address (per chain).trustedVersionId(or semver →versionIdmapping) per product line — from governance, release notes, or baked into the Bee binary.PostageStamp,Redistribution, etc.).proxyIdper core proxy as registered inVersionedRegistryRouter(same id used at deploy for that proxy row).Pin the implementation Bee trusts:
Cache
expectedImpland refresh on config change / reconnect (registry rows are immutable, but deprecation can flip;verifyProxy/pinnedExecutestill enforce “not deprecated” at call time).2. Reads (no gas) — health / startup
Use
eth_call:getRelease(trustedVersionId)verifyProxy(proxyId)— fails if proxy deprecated or implementation not registered / bad codehash / release deprecatedgetProxyImplementation(proxyId)and compare address equality withgetRelease(trustedVersionId).implementation(redundant with a strict pin policy but good for diagnostics)3. Writes (transactions) — recommended with guarded proxy
For each protocol call today you send to the proxy with calldata
C, instead send one tx:ProxyAdminrepointed the proxy to another registered implementation,pinnedExecutereverts (PinMismatch) even thoughverifyProxyalone would pass for the new impl—because Bee did not update its pin.4. Writes — legacy / stock
TransparentUpgradeableProxyBee can keep direct
Cto the proxy. No on-chain atomic pin; mitigate with maturity, timelocks, and re-reads before sensitive batches, or migrate deploys toRegistryGuardedTransparentUpgradeableProxy.5. Optional:
verifyImplementationUse when you have an implementation address from
getProxyImplementationor an EIP-1967 storage read and want a single call that enforces registration + non-deprecated + codehash without aproxyId.Security notes (short)
versionIdand point the proxy there. PinningtrustedVersionId+pinnedExecute(expectedImpl from that row)prevents accepting a different row’s implementation without the node updating config.fallbackalone only enforces “current impl is some valid release”;pinnedExecuteenforces “current impl is my expected address.”forward: convenient but wrongmsg.senderfor many core methods; use only where semantics allow.Deploy / ops
RegistryGuardedTransparentUpgradeableProxyfor core proxies when Bee should usepinnedExecute.(_logic, admin, initData, registry, registryProxyId); thenregisterProxy(registryProxyId, proxy)with the same id.registerReleasebefore or when adopting an implementation; upgrade order should avoid long windows where the proxy points at unregistered bytecode (guarded proxy reverts all user calls until registered).Tests
VersionedRegistryRoutertests cover registry,verifyProxy,verifyImplementation,forward, roles, invariants,RegistryGuardedTransparentUpgradeableProxyfallback paths, andpinnedExecute(success,PinMismatch, registry failure, admin rejection, bubbled revert).npx hardhat testafter merge (exact counts depend on branch).Test plan (PR checklist)
RegistryGuardedTransparentUpgradeableProxy+ documentproxyId/registerProxyorderingpinnedExecuteTx path + config fortrustedVersionId/expectedImplpinnedExecuteselector and no collision rule for new implementation ABIs