feat(versioned-proxy)!: Role 1 multi-admin whitelist#12
Merged
Conversation
BREAKING: replaces the single-slot proxyAdmin + 2-step ownership
transfer with a whitelist of proxyAdmins (mirrors Role 2 fee-admins).
Storage layout, constructor signature, and admin API all change.
Existing deployed proxies are incompatible — fresh deploys required.
Rationale: the user wanted Role 1 to feel symmetric with Role 2 (a
list of admins, instant grant/revoke). The 2-step transfer existed
to prevent fat-fingered transfers of the upgrade authority. That
safety is now delegated to putting a Safe multisig in the
proxyAdmins list — the Safe's internal quorum provides the
ceremony.
Contract changes (src/IntuitionVersionedFeeProxy.sol):
- Storage: drop `address proxyAdmin` + `address pendingProxyAdmin`,
add `mapping(address=>bool) proxyAdmins` + `uint256 proxyAdminCount`
- Constructor: `address admin` → `address[] initialProxyAdmins`
(reverts on empty list or any zero entry, dedupes silently)
- Functions: drop `transferProxyAdmin` + `acceptProxyAdmin`, add
`setProxyAdmin(address, bool)` with:
* idempotent reject (revert `ProxyAdminAlreadySet` if status
already matches — keeps the on-chain event log truthful)
* last-admin guard (revert `LastProxyAdmin` if revoke would
empty the whitelist — anti-lock-out)
- Modifier `onlyProxyAdmin`: mapping lookup instead of address ==
- Views: drop `proxyAdmin()` + `pendingProxyAdmin()`, add
`isProxyAdmin(address)` + `proxyAdminCount()`. The full admin list
is reconstructed from events (same pattern as Role 2)
Interface (src/interfaces/IIntuitionVersionedFeeProxy.sol):
- Events: drop `ProxyAdminTransferStarted/Transferred`, add
`ProxyAdminGranted(address)` + `ProxyAdminRevoked(address)`
- Function/view changes mirror the contract
Errors (src/libraries/Errors.sol):
- Drop `VersionedFeeProxy_NotPendingProxyAdmin`
- Add `VersionedFeeProxy_ProxyAdminAlreadySet` (idempotent reject)
- Add `VersionedFeeProxy_LastProxyAdmin` (anti-lock-out)
Factory (src/IntuitionFeeProxyFactory.sol):
- `createProxy` signature unchanged
- Now passes the full `initialAdmins[]` to the proxy constructor.
Both roles seed from the same list — diverge via setters later
if your team / Safe layout needs it.
Tests:
- All inline `VersionedFactory.deploy(scalar, ...)` updated to
`deploy([addrs], ...)` across 4 test files
- IntuitionVersionedFeeProxy.test.ts: 2-step transfer block replaced
by 7-test setProxyAdmin block covering grant, revoke, idempotent
guards (×2), last-admin guard, grant+revoke roundtrip, non-admin
reject. supportsInterface selector list updated.
- IntuitionFeeProxyFactory.test.ts: asserts both admins are now in
Role 1 (proxyAdminCount == initialAdmins.length)
226 tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… webapp Follow-up to a2c82e5 (contract-level Role 1 → multi-admin whitelist). Drags every downstream consumer onto the new API so the branch compiles and the UX matches the contract semantics. SDK - readers.readProxyVersions: drop `proxyAdmin` + `pendingProxyAdmin` outputs, return `proxyAdminCount: bigint` instead. Both the Multicall3 path and the per-field fallback are updated. safe-tx - New ops/versioned-proxy.ts exposing `setProxyAdmin(proxy, admin, status)` as an AdminOp builder (mirrors v2-admin.setWhitelistedAdmin). - Registered as `set-proxy-admin` in the CLI op-registry under a new `versioned-proxy` category. OP_REGISTRY count test bumped 10 → 11. - Unit test covers grant + revoke selector + args. webapp - useVersionedProxy: replace `useTransferProxyAdmin` + `useAcceptProxyAdmin` with `useSetProxyAdmin` + `useIsProxyAdmin`; add `useProxyAdmins` that reduces ProxyAdminGranted / ProxyAdminRevoked events into the current whitelist (no on-chain getter for the full set). - useProxyRoles: now reads `isProxyAdmin(account)` via wagmi instead of comparing against a single `proxyAdmin` address. - UpgradeAuthorityPanel rewritten as a multi-admin panel (list + per-row revoke + grant form + Safe-propose path), with a collapsible Advanced section housing the "grant both roles" convenience + the Role 1 vs Role 2 explainer. - AdminsTab + ProxyDetail: drop the obsolete `proxyAdmin` + `pendingProxyAdmin` prop plumbing. - useProxyAdminRotation hook deleted (2-step state machine no longer needed); stale AdminRotateStage type removed. - Docs.tsx: Architecture actor + Primitives + Admin Rotation sections rewritten for the whitelist model. - SafeProposeFeedback component extracted so every Safe-propose surface renders feedback identically. e2e - e2e-validate.ts step ⑫b: 2-step transfer flow replaced with whitelist grant → revoke → non-admin guard → last-admin guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves 5 conflicts triggered by main's Safe-aware refactors landing in parallel (PRs #10 + #11): - packages/safe-tx/src/ops/index.ts — keep both `versionedProxy` (ours) and `sponsored` (main) namespace exports. - packages/safe-tx/src/ops/versioned-proxy.ts — main shipped a versioned-proxy ops module with the OLD 2-step API (transferProxyAdmin + acceptProxyAdmin + registerVersion + setDefaultVersion). The 2-step API no longer exists on the contract post-a2c82e5. Resolution: keep our `setProxyAdmin` builder, keep main's `registerVersion` + `setDefaultVersion` builders (still valid — they're separate Role 1 surfaces), drop the 2-step ones. - packages/safe-tx/test/unit/ops/versioned-proxy.test.ts — same reduction: keep setProxyAdmin + registerVersion + setDefaultVersion tests, drop 2-step tests. - packages/webapp/src/components/SafeProposeFeedback.tsx — both branches independently extracted this component. Keep main's version (already reviewed in #10, slightly cleaner with fragment + parent-controlled spacing). - packages/webapp/src/components/UpgradeAuthorityPanel.tsx — main rewrote it as Safe-aware against the old 2-step API. All of that is dead. Take our multi-admin whitelist version entirely; the Safe-aware integration (useSafePropose + ops.versionedProxy) is already preserved in our version. Plus two test-infra fixes the merge surfaced: - safe-tx: ANVIL_PORT default 8545 → 8546. Anvil silently fails to bind a busy port and the fixture's RPC-ready probe was happily talking to a Hardhat node squatting 8545 from another package, producing opaque `anvil_impersonateAccount not supported` errors in the integration suite. direct-sign.test.ts bumped 8546 → 8547 to keep parallel runs collision-free. - safe-tx: broaden the trezor "no device" error regex in factory.test.ts to include "trezor getAddress failed — Transport is missing", which is what happens when the optional deps are installed but no Bridge / device is reachable at getAddress() time. Equally actionable to the other three branches the regex already accepts. Verified: 82/82 safe-tx tests pass, webapp typecheck clean, sdk build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
proxyAdmin+ 2-step transfer with aproxyAdminswhitelist mapping (same shape as Role 2 fee admins), with a last-admin guard.setProxyAdmin(addr, true/false), newisProxyAdmin(addr)view, newproxyAdminCount()view, newProxyAdminGranted/ProxyAdminRevokedevents.Files of interest
packages/contracts/src/IntuitionVersionedFeeProxy.sol— new storage + APIpackages/contracts/src/IntuitionFeeProxyFactory.sol— passes the full initial admins arraypackages/webapp/src/components/UpgradeAuthorityPanel.tsx— rewritten multi-admin UX with collapsible Advanced sectionpackages/safe-tx/src/ops/versioned-proxy.ts— newsetProxyAdminAdminOp builder + CLI entrypackages/sdk/src/readers.ts—readProxyVersionsnow returnsproxyAdminCountTest plan
bun --filter @intuition-fee-proxy/contracts test— 226 tests passbun --filter @intuition-fee-proxy/safe-tx test— 72 tests passbun --filter @intuition-fee-proxy/webapp typecheck— cleanbun --filter @intuition-fee-proxy/sdk build— cleanbun --filter @intuition-fee-proxy/contracts e2e:local— end-to-end fork validation, including the new step ⑫b (grant → revoke → guards)0x48f5829E3F2116F639B6baB1cc810AC9b8ae3e32Notes for reviewers
ProxyAdminGranted/ProxyAdminRevokedevents ordered by(block, logIndex)— seeuseProxyAdmins.🤖 Generated with Claude Code