feat(access-v2): unified membership authority and safe migration - #193
feat(access-v2): unified membership authority and safe migration#193hudsonhrh wants to merge 102 commits into
Conversation
…hadowing audit (W4) Append `address configAdmin` at each module's ERC-7201 Layout tail with an executor-gated setConfigAdmin(address) + ConfigAdminSet event. Widen auth to `executor || configAdmin` only on: TM setConfig ROLE_PERM branch (all other ConfigKey branches stay executor-strict), PT setMemberHatAllowed/ setApproverHatAllowed, Edu setCreatorHatAllowed/setMemberHatAllowed, and QJ updateMemberHatIds. Enables a RoleManager to fan out role wiring without being the executor. Adds read-only AuditProjectRolePermShadowing script that reports per-project ROLE_PERM overrides shadowing global masks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New per-org BeaconProxy module implementing first-class named roles and role groups, driven entirely through governance (onlyExecutor on every mutation, zero user-facing mutating functions). Creates identity/marker hats via the EligibilityModule, fans typed permission wiring out to sibling modules (DD/HV/TM/PT/Edu/QJ) + per-hat paymaster budgets, and implements the consent model: in-org members are minted directly, non-members receive an explicit eligibility offer (RoleOffered) they accept via EM.claimHats. Revocation is a single clearWearerEligibility on the defaultEligible=false identity hat, with group markers auto-following via derived eligibility (no burn). - src/interfaces/IRoleManager.sol: frozen structs/events/ABI (W2 section) - src/RoleManager.sol: ERC-7201 storage, _disableInitializers, initialize mirrors seed events - test/RoleManager.t.sol + mocks: 24 tests (lifecycle, consent matrix, wiring fan-out, derived-revocation, registerExisting, auth) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss edits (W3)
Additive-only ABI for RoleManager Phase 2. Existing createProposal
selectors/events/behaviour are byte-identical; new surface lives in side
mappings appended at each Layout tail (never on the Proposal struct).
DirectDemocracyVoting:
- createProposalV2(..., uint32 quorumOverride): restricted-only override;
executable proposals raise-only (effectiveQuorum = max(global, override),
crit-security H-2), signal polls may lower.
- setConfigAdmin + configAdmin gate on setConfig(HAT_ALLOWED) only.
- ProposalConfigV2 event alongside NewProposal/NewHatProposal.
HybridVoting (+ libs):
- createProposalV2(..., uint32 quorumOverride, bool equalWeight): equalWeight
snapshots one synthetic DIRECT class {slicePct:100, hatIds: pollHatIds} so
vote()/announceWinner() are untouched and tally is one-person-one-vote.
- addHatToClass/removeHatFromClass (executor||configAdmin, ClassHatSet event,
bounds-checked, slices untouched); setCreatorHatAllowed/setClasses accept
configAdmin; setConfigAdmin executor-only.
- announceWinner honours the effective quorum.
Tests: DirectDemocracyVotingV2.t.sol (17) + HybridVotingV2.t.sol (24) cover V1
regression, H-2 raise-only, signal-poll lowering, equalWeight one-person-one-vote
vs whale control, class-edit auth matrix + bounds, and storage-layout safety.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er authority (W1) Adds the 4th eligibility path (derived/group membership) plus a scoped RoleManager admin surface to EligibilityModule, per RoleManager Phase 2 W1. - getWearerStatus gains an additive derived branch (mirrors the email branch): a group marker hat with a member-hat list makes any wearer of a listed identity hat eligible+standing, unless an explicit per-wearer rule (kick) says otherwise. Hats with no derived config are byte-identical. - setGroupEligibility with bidirectional derived<->vouch guard (M-03 pattern) and flat one-level nesting via groupMembershipRefCount (cycle/OOG safety). - claimHat / claimHats(uint256[]) permissionless self-mint requiring a SPECIFIC eligibility source (explicit rule / vouch quorum / email / derived) — bare default-open reverts NotClaimableHat (H-03). claimHats is the offer-acceptance path (identity+markers in one tx, cap 20). - Scoped onlySuperAdminOrRoleManager gate on the whitelisted wiring/config functions only; setRoleManager stays superAdmin-only; grantWearerEligibility is grant-only (true,true), bans remain superAdmin. Adds updateHatConfig and createHatWithEligibilityChecked (expectedHatId drift guard). - Storage appended after emailVerified (roleManager, groupMemberHats, groupMembershipRefCount). Heavy logic offloaded to a linked delegatecall library (EligibilityLogic) to stay under EIP-170 (module 22,522 B, +2,054). - Tests: EligibilityModuleDerived.t.sol (36) + MockHatsEligibilityAware (faithful eligibility!=balance model). Updated pre-existing NotSuperAdmin assertions to NotSuperAdminOrRoleManager for the re-gated functions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pped (integration fix) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opt-in RoleManager module on the atomic org-deploy path, mirroring the
ZkEmailInvites pattern (beacon-gated, register-before-initialize).
- ModuleTypes: add ROLE_MANAGER_ID = keccak256("RoleManager").
- ModulesFactory: RoleManagerConfig{enabled}; deploy the proxy UNINITIALIZED,
gated on config.enabled AND beaconRegistered(ROLE_MANAGER_ID) (skip, never
brick the org deploy); include it in the batch registration. Initialization
is deferred to OrgDeployer because RoleManager.initialize needs the DD/HV
voting addresses, which are deployed after ModulesFactory returns.
- Executor: configureModule(target,data) onlyOwner — generic bootstrap relay
(C-01 pattern; dead on live orgs where owner()==0). Emits ModuleConfigured.
- OrgDeployer: deployFullOrgWithRoleManager sibling entrypoint (DeploymentParams
tuple untouched); DeploymentResult gains roleManager. After voting deploy and
before renounce, initialize the RoleManager and wire scoped authority through
the Executor: EM.setRoleManager + setConfigAdmin on DD/HV/TM/PT/EduHub/QuickJoin.
- HatsTreeSetup: decouple deploy-time minting from canVote (mintToDeployer /
additionalWearers mint regardless of canVote); OrgDeployer._collectInitialWearers
mirrors this so the InitialWearersAssigned event stays consistent.
- Tests: test/OrgDeployRoleManager.t.sol (9) — registration under ROLE_MANAGER_ID,
seeded orgHats init, EM.roleManager + configAdmin on all six modules,
register-before-initialize ordering, disabled-config parity, beacon-unregistered
skip, EduHub-disabled wiring, and the canVote=false+mintToDeployer mint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ogic layout-sync guard (W6) Salvaged from interrupted W6 agent worktree; verified 20/20 passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- revokeRole: burn via checkHatWearerStatus (frees capped-hat supply for elections), reconcile group markers, revert RevokeIneffective when eligibility persists (default-eligible/vouch/email) instead of emitting a false RoleRevoked - _applyWiring guards: reject vouch+combine=false (breaks grant/offer model), vouching on group markers, and quickJoinAutoMint on default-closed hats (would brick QuickJoin joins) - offer branch clears stale explicit marker rules so one-tx claimHats acceptance survives old kicks - EligibilityModule: distinct WearerEligibilityCleared event (clears were log-indistinguishable from bans) - RoleManager: ModulesWired init event + modules() view (fan-out targets reconstructable from logs) - tests: capped-role election supply regression, RevokeIneffective on adopted default-eligible roles, offer-after-kick claim, wiring guards, tightened GroupCreated assertion, PT/Edu/QJ non-widened-setter negatives Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AdoptRoleManagerTest6.s.sol adopts the RoleManager module into the live
Test6 org (Gnosis). Three contracts per PLAN §2.3 / deploy-flow §3:
- PredeployTest6 (EOA): SwitchableBeacon(executor, protocolBeacon, 0, Mirror)
+ uninitialized BeaconProxy.
- BroadcastAdoptTest6: the single 9-call governance batch (registerOrgContract
-> RoleManager.initialize -> EM.setRoleManager -> setConfigAdmin x6 on
DD/HV/TM/PT/EduHub/QuickJoin), submitted via HybridVoting.createProposal.
Includes the C1 pre-flight uninitialized-proxy guard.
- SimAdoptTest6: full Gnosis fork sim — in-fork protocol prereqs (Satellite
addContractType + Mirror beacon wave, pranked as the real owner), predeploy,
create/vote/warp/announceWinner (real creator-hat wearer, explicit 8M gas
stipend), post-asserts (RegisteredContract, modules() match, EM.roleManager +
6 configAdmins == proxy), and a createRole("Canary") smoke via a 2nd proposal.
Sim PASS under FOUNDRY_PROFILE=production; adoption announceWinner used 977,855 gas.
Test6 modules verified Mirror-mode; batch <= 20 calls, no Executor self-target.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register the RoleManager module at the protocol level per-chain and sponsor
the EligibilityModule self-claim path in the global rulebook.
- script/rolemanager/RegisterRoleManagerProtocol.s.sol: per-chain (Gnosis via
Satellite, Arbitrum via Hub, both LOCAL — never Hyperlane) CREATE2-deploy the
RoleManager impl (computeSalt("RoleManager","v1")), addContractType (creates the
global beacon + registers v1), deploy the RoleManager-aware ModulesFactory +
OrgDeployer (v21) and setModulesFactory / upgradeBeacon. Broadcast + Sim pairs;
the Sims prank Hudson and assert getBeaconById / getCurrentImplementationById /
modulesFactory-slot before AND after. Versions dual-surface probed FREE on both
chains (RoleManager v1, OrgDeployer/ModulesFactory v21).
- script/helpers/DefaultGlobalRules.sol: add (ELIGIBILITY_MODULE_ID, claimHat) and
(…, claimHats) entries (52 -> 54) with batch-sized gas hints (300k / 3M for the
up-to-20-mint loop); selectors derived from the real ABI, trailing assert honest.
- script/rolemanager/SyncRoleManagerGlobalRules.s.sol: broadcast the two-rule delta
(read from DefaultGlobalRules so hints stay single-sourced) via Satellite/Hub
adminCall; Sim stands up v20 on the fork (live hub is still v19), seeds the pre-W7
baseline, then asserts getGlobalRule before/after with the correct Rule field order
(maxCallGasHint, allowed) and count 52 -> 54.
- test/PaymasterGlobalRules.t.sol: extend the selector-bijection expectation to 54.
Sims run under FOUNDRY_PROFILE=production against gnosis + arbitrum forks — all PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add script/rolemanager/UpgradeModulesForRoleManager.s.sol upgrading the 8 impls changed by the RoleManager feature (EligibilityModule, DirectDemocracy- Voting, HybridVoting, TaskManager, ParticipationToken, EducationHub, QuickJoin, Executor) across Gnosis (Satellite-local) and Arbitrum (Hub-local). - Dual-surface version probe (registry + CREATE2) per type, both chains; chosen versions recorded in the header (EM v8, DDV/HV v13, TM/PT/QJ v8, EduHub v4, Executor v5) — all FREE on both chains, identical CREATE3 addrs. - External libs (EligibilityLogic; HybridVoting's 3 libs) auto-linked into type().creationCode by Foundry, deployed ahead of impls — same convention as UpgradeGovernanceSecurity.s.sol; CREATE3 keeps impl addrs chain-independent. - Broadcast steps (Gnosis deploy, Arbitrum cross-chain upgrade, Gnosis direct upgrade, verify) + SimGnosis/SimArbitrum pranking Hudson. - Sims assert per type: getCurrentImplementationById flips AND a live Mirror org proxy (Test6/Gnosis, Poa/Arbitrum) follows the beacon with byte-identical storage-backed reads; EM roleManager()==0 + getWearerStatus unchanged proves the derived-eligibility path is inert-until-configured. Both sims PASS under FOUNDRY_PROFILE=production on live gnosis/arbitrum forks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…omization-via-vote
…riant Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hority, registration guards - DefaultGlobalRules 54->56: sponsor DD/HV createProposalV2 (passkey users keep gasless restricted-poll creation); sync-script delta generalized to the 4 RoleManager-wave rules via _isDeltaEntry - OrgDeployer: grant+mint the org's paymaster operator hat to RoleManager at deploy (setBudget is onlyOrgOperator — budgets were silently BudgetSkipped); best-effort with RoleManagerOperatorHat event - RoleManager: maxSupply 0 => unlimited (raw 0 is unmintable in Hats); duplicate initial group members revert AlreadyInGroup; one role per hat (HatAlreadyRegistered); one group per marker + role/marker hats mutually exclusive via groupIdOfMarker index (MarkerAlreadyRegistered) - tests: 6 new guards; PaymasterGlobalRules bijection 54->56 All 7 production fork sims re-run PASS on both chains after the changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ogic lib Move _applyWiring/_applyQuickJoin into a new delegatecall library (src/libs/RoleManagerLogic.sol) with a byte-identical ERC-7201 Layout mirror + functional sync-guard test; RoleManager runtime drops from 23,061 B (margin 1,515) to 19,258 B (margin 5,318) at runs=200. Add ManagerConfig storage (roleManagerConfigs/groupManagerConfigs) and onlyExecutor setRoleManagerConfig/setGroupManagerConfig with the SelfManagedGroup direct-cycle guard, re-checked in addRoleToGroup. grantRole/revokeRole gain delegated auth (executor OR wearer of the role's or any containing group's managerHat with the needed cap) + nonReentrant. Delegated paths are provenance-guarded via EM.getWearerRuleFlags: grant reverts GrantBlockedByGovernanceBan when it would overwrite a governance-owned ban (may clear a 0x04 delegated kick); revoke refuses to clear a governance-written (true,true) offer. Executor paths are byte-identical to today. REPLACED RoleGranted/RoleOffered/RoleRevoked event signatures with actor+delegated fields (pre-broadcast, per INTERFACES.md W13) and add RoleManagerConfigSet/GroupManagerConfigSet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e, kick delegation
FIX 0 (explicit-ban supremacy): EligibilityLogic._getWearerStatus short-circuits
to (false,false) on any explicit rule with bits 0-1 clear, before the vouch combine,
so a governance ban (or delegated kick) beats vouch/email/derived/default on EVERY
path. Mirrored in _hasSpecificEligibilitySource. Explicit (true,true) keeps additive OR.
FIX 1 (provenance): WearerRules.flags bit2 (0x04) = DELEGATION_MANAGED. Set by kicks,
unkick restore, and RM-mediated grantWearerEligibility; auto-cleared by every direct
superAdmin writer (correct-by-construction via _packWearerFlags). New getWearerRuleFlags
view for RoleManager (W13) provenance checks.
Primitive B (kick delegation): KickConfig{kickerHatId,delaySecs,enabled} + pending-kick
lifecycle — configureKick/kickWearer/finalizeKick/cancelKick/unkickWearer. Governance
supremacy (CannotKickGovernanceRuled), effect delay (pending kicks die with the delegation),
honest KickIneffective belt, exact-0x04 unkick restoring (true,true|0x04). All kick bodies
in EligibilityLogic; EM has thin wrappers. Rule writes void pending kicks.
Sizes (production): offloaded claimVouchedHat + vouchFor/revokeVouch bodies to the lib to
hold margin — EligibilityModule 21,516 B (margin 3,060), EligibilityLogic 9,429 B
(margin 15,147).
Tests: new EligibilityModuleDelegation.t.sol (35 tests: supremacy truth table, provenance
lifecycle across all six writers, kick governance-supremacy, immediate/delayed/cancel/unkick,
vouch survival + KUBI regression); extended EligibilityLogicLayoutSync for kickConfigs/
pendingKicks + RM-grant 0x07. Updated DeployerTest.testVouchingSystemHybridMode Test 3
(asserted the now-fixed OR bug).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…omization-via-vote
…pe, Test6 batch v10 - DefaultGlobalRules: kickWearer/finalizeKick/unkickWearer (EM) + grantRole/revokeRole (RoleManager) with measured gas hints - OrgDeployer._buildTargetTypes: ROLE_MANAGER_ID branch (delegated lifecycle calls are user-facing now) - AdoptRoleManagerTest6: 10-call batch adds setTargetTypesBatch for the RM proxy; sim stands up hub v20 in-fork (broadcast order unchanged) - SyncRoleManagerGlobalRules: delta generalized to the 9 wave rules - tests: bijection 56->61, hinted-entry set, RM target-type coverage Full suite 2028 green; all 7 production fork sims re-run PASS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rototype Head-to-head gas benchmark for the "remove Hats + unified authority" blank-slate decision (.context/rolemanager/blank-slate-review.md, PLAN.md). - test/AccessV2GasBench.t.sol: measures 5 hot paths against the REAL wiring (forked Gnosis Hats + real EligibilityModule/EligibilityLogic + real RoleManager) and the same 5 against the prototype — single-check, derived group check (n=1/3/5), DD/HV vote hasAnyHat (n=2/5/10), permMask, and grant/revoke lifecycle — reporting cold and warm gas. - test/mocks/NativeLedgerPrototype.sol: faithful minimal collapsed authority (membership = accepted-bit && eligible-policy, the full five-path resolution ported by logic shape from EligibilityLogic, role/group registry, permTable, isMember/hasPerm surface, ERC-7201 storage, events on writes). Production runtime 7,517 B. - test/mocks/CurrentPathHarness.sol: real HatManager.hasAnyHat + verbatim TaskManager._permMask over real Hats (bodies otherwise internal-only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the round-1 collapsed-authority floor bench into a production-weight
prototype (MembershipAuthorityProto) implementing every ACCESS-V2-SPEC v1.3
shape that moves gas/size, and re-measures against the real Hats/EM/RM path.
- §3 inverted-fold hasPerm (subjectsWithKey index, ctx∪global deduped union,
packed {exists,inheritGlobal,254-bit value} word, permKey-top-byte fold tag);
measured at fan-outs 2/8/32 with role+group mix, groups at GroupSizeLimit.
- §3 activeMemberSince(subject) + key-folded activeMemberSince(user,key,ctx).
- production weight: ERC-1155 TransferSingle + six-plus-RoleClaimed lifecycle
events, pause + reentrancy guards, executor-root + ManagerConfig scoped
access, unified pending-action table (create/finalize/cancel) with delaySecs,
memberCount/maxMembers, caps (16 roles/user, 16 members/group, 8 groups/role,
16 subjects/key) enforced with named errors, IHats read-subset + viewHat(9).
- re-measured round-1 ops + delegated grant (pending create+finalize), claim
(offer-accept), reconcile; warm+cold columns.
Size (production profile): 16,808 B runtime — 7,768 B under EIP-170, ~4.2 KB
under the spec's 21 KB lib-split trigger; one contract fits with margin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Protocol-owned singleton router per ACCESS-V2 freeze §2 + ruling R5: - TOTAL structural classification predicate (legacy ≥2^224 bound→authority / unbound→Hats passthrough; v2 embedded-address self-route; codeless/malformed →empty resolution; sub-2^64 garbage→safe-zero passthrough) - OrgRegistry-gated legacy bind/unbind with topHat-domain verification (executor via OrgRegistry.orgOf, domain vs getTopHat>>224) - served IHats read subset (isWearerOfHat/isEligible/balanceOf/balanceOfBatch/ getWearerStatus/viewHat 9-field/checkHatWearerStatus) - msg.sender==PaymasterHub revert-wrapping (clean false/0 for the hub; honest propagation for others; v2 self-routes always empty-on-fail) - R5: checkHatWearerStatus passthrough uses CALL not STATICCALL (declared non-view; selector identical, ABI unchanged) Router runtime size 6,437 B (production) — well under the 21KB lib-split trigger; no delegatecall partition needed. test/AuthorityRouter.t.sol: 46 tests — full degenerate-input matrix, bind auth (real OrgRegistry fixture, spoof/domain-mismatch rejection), passthrough byte-parity, hub-wrapping differential, unbind/re-bind rollback, unbind-mid-batch, two-org routing isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eze §0/§1/§3/§5) Implements the frozen MembershipAuthority build contract (ACCESS-V2-INTERFACES.md, ACCESS-V2-SPEC.md v1.3): one per-org ERC-7201 BeaconProxy authority folding membership, the tri-state eligibility resolver, the inverted semantic-perm fold, unified pending-action delegation, seeds/migration and the Hats/ERC-1155 read surface. - Shared types/ids/perm-key libs (AccessV2Types/Ids/PermKeys); packed Membership slot; authored-by + delegable rule slot; vouch attestor with epochs/rate limits/admin surface; email attestor; per-subject defaults with force flag. - Perm table: packed word, subjectsWithKey inverted index, ctx∪global dedup union fold, both activeMemberSince variants, canGrant/canRemove/canClaim preflights with lapsed hints. - Delegation: ManagerConfig, monotonic pendingId, SelfManagedGroup guard, NotYetActive anchor, finalize/cancel/void; soft/hard removal with RemovalIneffective(sourceSet) + SubjectFull hint. - Pause redefinition (executor writes exempt), born-paused initialize mirroring all seed events, IHats read subset incl. IHats-shaped mintHat, ConfigLint events, R3 ops views. Size gate (FOUNDRY_PROFILE=production, EIP-170 24,576): the combined cold surface exceeded the ceiling (single lib 24,975 B), so it is split across TWO delegatecall libs sharing the ONE canonical Layout: MembershipAuthority hub 23,301 B, MembershipAuthorityLogic 20,431 B, MembershipAuthoritySeed 9,449 B. Slot-mirror sync test pins the ERC-7201 constant across all three units. Tests: fold truth table, membership state machine, pending lifecycle, caps + SubjectFull hint + reconcile, seeds-while-paused, emission-contract conformance, union fold with inherit flags, id arithmetic + legacy adoption + two-authority subjectKey distinctness, activation gate, layout sync. 55 new tests green; full suite 2085 passed (--no-match-contract DeployerTest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rity remap Wave B workstream B3: remap EducationHub, QuickJoin, Executor and PaymasterHub onto the MembershipAuthority per ACCESS-V2-INTERFACES §4.5–§4.8. - EducationHub: append `membershipAuthority` + executor-gated setMembershipAuthority; creator/member gates dual-path to EDU_CREATE / EDU_MEMBER hasPerm folds when set, byte-identical legacy Hats path when zero. - QuickJoin: append `membershipAuthority` + setter; join path enumerates the QJ_AUTOJOIN default-ALLOW subjects via subjectsWithKey and mints through the unchanged Executor loop; legacy memberHatIds path byte-identical when unset. - Executor: setMembershipAuthority repoints the `l.hats` slot itself (§4.7), stashing the original in an appended `legacyHats` field so setMembershipAuthority(0) restores it (rollback). Selector added to the execute() self-target allowlist so a renounced org can repoint through governance. Emits HatsRepointed. - PaymasterHub: one change — protocolAdmin/poaManager-gated setHats(address) + HatsSet event (§4.8 / §6 step 0.3 router repoint); validation semantics untouched. Dual-path + regression + rollback tests per module; QJ join equality differential (legacy hats list vs authority QJ_AUTOJOIN subjects → same mints); Executor repoint/restore; hub setHats auth matrix. All storage appends are ERC-7201 tail-only. Production --sizes (runs=200, --skip test): EducationHub 9,107 / margin 15,676; QuickJoin 10,665 / 14,118; Executor 7,439 / 17,344; PaymasterHub 22,602 / 1,974 (+184 B vs on-branch baseline 22,418). Full suite: 2164 passed, 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Append MembershipAuthority dual-path to TaskManager and ParticipationToken per ACCESS-V2-INTERFACES.md §4.1/§4.4/§4.5. When membershipAuthority == 0 each module reads its legacy Hats path byte-identically (rollback state); when set, permission reads route through the authority. TaskManager: - Layout tail: append `address membershipAuthority` - setMembershipAuthority(address) onlyExecutor + MembershipAuthoritySet event - _permMask authority arm: uint8(hasPerm(user, TM_PERMS, bytes32(pid))) (OR-mask fold; §3 CTX RESOLUTION replaces the v1 REPLACE-shadow footgun) - creator/organizer checks fold isMember over the existing subject-id enumerations (hat ids adopted verbatim, §1) so migrated orgs keep working - getLensData variant 12 surfaces the pointer - production runtime 22,013 B / 2,563 B margin (was 21,168 / 3,408; +845 B, no offload) ParticipationToken: - Layout tail: append `address membershipAuthority` - setMembershipAuthority(address) onlyExecutor + MembershipAuthoritySet event - membershipAuthority() getter - _hasHat authority arm: hasPerm(user, PT_MEMBER|PT_APPROVE, GLOBAL) != 0 - production runtime 15,272 B / 9,304 B margin Tests (built against the REAL MembershipAuthority): - legacy regression, authority-path mask parity (global/project-replace/inherit), §5.2 #8 effective-mask equality differential (legacy vs authority, exhaustive over (user, project)), creator/organizer routing, PT member/approver dual-path, setter auth + rollback-to-zero. Existing TaskManager/ParticipationToken suites unchanged and green; full suite 2147 passed (DeployerTest excluded per fork-RPC noise). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…W-B1) Wave B workstream B1: remap DirectDemocracyVoting + HybridVoting onto the MembershipAuthority behind the dual-path invariant. `membershipAuthority == 0` reads the legacy Hats path byte-identically (unmigrated orgs / rollback); when set, permission reads route through the authority per the frozen key shapes plus the electorate activation gate. DD (src/DirectDemocracyVoting.sol): - Layout tail: `membershipAuthority` + `proposalCreatedAt` side-mapping. - executor-gated setMembershipAuthority + MembershipAuthoritySet event. - create → hasPerm(DD_CREATE); vote → activeMemberSince(DD_VOTE) <= createdAt. - restricted polls: pollHatIds reused id-agnostically as subject ids, each activation-gated via activeMemberSince(subject, voter) <= createdAt. HV (src/HybridVoting.sol + Core/Config/Proposals libs): - Layout tail (Proposal struct + ClassConfig stride NEVER touched): membershipAuthority, classSubjectSeq, classIdOfIdx, classSubject, proposalClassSubjects side-mapping, proposalCreatedAt. - setClassSubject(classIdx, subjectId) [executor||configAdmin] with frozen stable-classId allocation; classIdOfIndex/classSubjectOf/proposalClassSubject views; ClassSubjectSet event. - create → hasPerm(HV_CREATE); class membership → immutable per-proposal subject snapshot + activeMemberSince(sid) <= createdAt (group-packer caught); legacy hat-gated power path unchanged when authority is 0. - DEVIATION (documented): class membership resolves via the per-proposal snapshot ONLY (no live classSubject fallback at vote time) so the binding is immutable across later setClassSubject edits (§5.2 snapshot-immutability obligation); snapshot-0 classes / equalWeight fall back to hatIds-as-subjects. Tests: per-module dual-path suites (legacy regression stays green, 2144 pass): creator gating, the mid-proposal activation gate (excluded from the open proposal, eligible on the next), restricted-poll subjects, HV class-subject snapshot immutability + allocation semantics, legacy↔authority equality differential, setMembershipAuthority auth + rollback-to-zero. Adds test/mocks/MockMembershipAuthority.sol. Sizes (production, runtime): DD 14,013 B (10,563 margin), HV 9,729 B (14,847 margin) — inline, no lib offload needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… read path MainDeploy (home + satellite) and DeployInfrastructure initialized the PaymasterHub with real Hats and never deployed an AuthorityRouter, so on any chain stood up by these scripts every Wave-F org's admin surface was dead on arrival: the hub's adminHatId is a new-style authority subject id (< 2^224), which real Hats resolves to balance 0. withdrawOrgDeposit (no poaManager bypass), setPause and setOperatorHat all reverted NotAdmin forever. - DeployHelper._wireAuthorityRouter: deploy the router singleton (ERC1967Proxy, fully initialized — never an uninitialized proxy) and repoint the hub via the poaManager-gated setHats. Documents why the hub cannot simply be *initialized* with the router: the router's own initializer needs the OrgRegistry and hub addresses, so the cycle is broken by init-then-repoint. - Register the AuthorityRouter impl (provenance, mirroring the live ceremony) and — mandatory, previously missing — the MembershipAuthority type, without which AccessFactory.deployAuthority reverts and no org can deploy at all. - Serialize the router address into the deploy state files. - DeployerTest: end-to-end coverage with the router in the loop — a deployer-produced org's ADMIN-subject wearer drives setPause/setOperatorHat through the hub, a stranger still cannot, and the pre-repoint revert is pinned. Fork-simulated on a Gnosis fork: DeployInfrastructure and MainDeploy:DeployHomeChain both complete, including a full governance-org deploy (15 contract types). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…2 + ceremony) OrgRegistry.updateOrgMetaAsAdmin authorizes against `l.hats`, which was written only at initialize with no setter. Every new-style org seeds its metadata-admin hat with an authority SUBJECT id (< 2^224) that real Hats resolves to balance 0, so the direct (non-governance) org-metadata edit path was dead for them on every chain — while the deployer still wired the feature and the paymaster still type-mapped OrgRegistry for that very selector. - OrgRegistry v2: `setHats(address)` mirroring PaymasterHub v20 (zero-check, event, admin gate) + a `HatsSet` mirror inside initialize so indexers see the deploy-time pointer. The gate is the registry owner (OrgDeployer, or the deployer EOA during genesis) OR the PoaManager — resolved as the owner of this proxy's own ERC-1967 beacon, which is exactly what Hub/Satellite adminCall reaches; the registry stores no poaManager pointer, and a live-chain upgrade has no way to seed one. - Fresh-chain deploys repoint the registry alongside the hub. - RegisterAccessV2Protocol: OrgRegistry v2 impl (CREATE3), beacon upgrade, and one adminCall setHats(router) per chain — upgrade strictly before repoint, since setHats ships in v2. Version dual-surface probed FREE on both chains (registry count 1, CREATE3 slot 0x72b453a3… empty). - Both production-profile fork sims re-run and PASS, now asserting the registry pointer before/after and driving a live updateOrgMetaAsAdmin as a REAL metadata-admin wearer (Test6 / Poa) on both sides of the repoint, with a stranger still rejected. - Runbook step 1 row updated. Sizes (production, --skip test): OrgRegistry 7,548 B runtime / 17,028 B margin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eploy time QJ_AUTOJOIN is only reachable for a default-ALLOW role: QuickJoin enumerates subjectsWithKey(QJ_AUTOJOIN) with no eligibility filter and the authority then rejects a non-eligible stranger, so a deny-by-default role in quickJoinRolesBitmap deploys cleanly and then reverts EVERY quickJoin call until governance intervenes. The deployer is the natural enforcement point — the pairing is now checked in _validateRoleConfigs with a custom error naming the offending role index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r-cap up front Duplicate memberRoleIndices in a GroupConfig reverted SubjectExists, and a maxMembers cap below a role's genesis wearer count reverted SubjectFull — both from inside the authority's BeaconProxy constructor, surfacing as opaque deploy failures rather than the deployer's own validation error. Both are now rejected in _validateRoleConfigs with custom errors naming the offending indices. The over-cap pre-check uses the exact seeded count (mintToDeployer + additionalWearers); a repeated address in that set already fails hard with AlreadyMember, so the bound has no false positives. Sizes (production, --skip test): OrgDeployer 23,213 B runtime / 1,363 B EIP-170 margin (was 22,940 / 1,636 before F3+F4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e path docs/ORG_DEPLOYER.md still walked through the deleted architecture: "Step 6: Register Hats Tree", registerHatsTree(topHatId, roleHatIds), a DeploymentParams with the removed defaults/hierarchy/hatConfig fields, and eligibilityModule / toggleModule wiring. README.md linked to the deleted src/HatsTreeSetup.sol twice and listed its deployed address; script/README.md claimed the infra script deploys a "HatsTreeSetup helper" that no longer exists. - ORG_DEPLOYER.md rewritten against the current 15-step deployFullOrg: subjects instead of hats (with the id derivation and seq table), the born-initialized authority, the router read path, the current RoleConfig/GroupConfig, the new validation errors, and the real OrgDeployed signature. - README.md: HatsTreeSetup rows/links removed, factory descriptions corrected, MembershipAuthority added to the org layer, EligibilityModule/ToggleModule marked legacy-only, and the access-control section now leads with Access v2. - POP_OVERVIEW.md: architecture diagram and role section updated. - script/README.md: infra list corrected (router + rulebook in, HatsTreeSetup out) and the infrastructure.json path fixed. No dead links remain in the touched files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lobalRules The comment claimed claimHat/claimHats were dropped because "no chain ever had a targetTypes row mapping them". That is wrong: targetTypes rows map module ADDRESSES to typeIds, not selectors, and UpgradePaymasterGlobalRules Step4 backfills every live org's EligibilityModule address, which would make ALL EM-typed rows resolvable. This file is the declared source of truth for the setGlobalRulesBatch broadcast, so a false premise here invites a wrong call during the Wave-G strip. Replaced with the reasons that actually hold: the v20 type-keyed rulebook has never been broadcast (both hubs are still v19), the legacy targetTypes backfill was never broadcast either (Wave-F review finding P5), and the two selectors exist only on the unshipped EligibilityModule v8 impl — plus an explicit warning not to generalise the argument to the 8 retained EM rows, which cover selectors that do exist on live impls. The mirrored comment in test/OrgDeployerPaymasterRules.t.sol now points at it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… summary and infra JSON MainDeploy no longer serializes a "hatsTreeSetup" state key, so deploy-testnet.sh printed "HatsTreeSetup: null" for both the home chain and the satellite. The committed script/config/infrastructure.json also still carried the key with an old deployed address, misrepresenting the current infra set to anything consuming that file. Both summaries now print the chain's AuthorityRouter instead — the address that actually matters for the Access-v2 read path, and which MainDeploy now writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dangling provenance pointers only (nothing compiled against the deleted names):
test/AccessV2Acceptance.t.sol cited test/RoleManagerIntegration.t.sol,
script/upgrades/UpgradeOrgDeployerZkEmailRules.s.sol told the operator to land
before broadcasting script/org/DeployComfiestHouse,
test/mocks/MembershipAuthorityProto.sol cited {NativeLedgerPrototype}, and
test/EligibilityModuleM03.t.sol cited HatsTreeSetup.batchSetDefaultEligibility.
Each now describes what the reference was without linking a 404.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Setup snapshots upgrades/baseline, upgrades/current and upgrades/previous all still contain HatsTreeSetup.sol after the source deletion. They are inert — the upgrade-safety job only activates once .github/upgrades.json exists (it does not, per .github/workflows/ci.yml), upgrades/ is on no forge build path, and test/UpgradeSafety.t.sol never reads them. CLAUDE.md and the README both forbid hand-editing upgrades/ (CI-generated), so the orphans are left in place and flagged in the README instead, with an explicit instruction for whoever wires up .github/upgrades.json not to list the dead contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…it), fresh gas table, W4 ctx docs, CLAUDE.md v2 story - _lower/_lowerName copied strings instead of mutating in place: the in-place aliasing corrupted OrgSpec.name, so the KUBI==5M announceWinner branch never matched and its proposal JSONs carried 4M. Verified end-to-end: JSONs now 5M. - Gas table re-measured on all four orgs (batches grew after A6: KUBI now 5 seed proposals, max 2.96M; creation max 5.06M -> wallet floor ~6M); runbook gains env pre-flight, exact broadcast entrypoints, per-org chain map, the enumerate-tm-perms fixture requirement, the delta-regen seed-file caveat, and a frontend go/no-go note for real orgs. - AccessV2PermKeys + spec: TM per-project ctx documented as projectId + 1 (W4) — the constants file taught the pre-amendment convention that would misplace a hand-authored project-0 row at GLOBAL scope. - CLAUDE.md now tells the Access-v2 story (authority model, router, deploy path, legacy-arm-until-Wave-G warning); script/README paths fixed; proto mocks marked HISTORICAL; error-reuse documented on the frozen interface; stale HatsTreeSetup comments scrubbed; OrgRegistry registerHatsTree annotated; interfaces/spec context docs gain the ccbc029 event-law amendment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the unpranked deploy path Running Step1 WITHOUT --broadcast dry-runs successfully and deploys nothing; Step2's pre-broadcast _deployImpls() re-derivation then fell into dd.deploy with forge's default sender and died with a cryptic OwnableUnauthorizedAccount (hit live by Hudson on Gnosis — verified all six CREATE3 slots empty, nothing was broadcast). Step2/Step2b in both protocol scripts now use _requireImpls (computeAddress + require code) so the failure is the instruction itself: run Step1 WITH --broadcast. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dow warning Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed decode shipped (PR #485) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enerated JSON) + VoteMigrationProposal Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eate/vote/wait/announce/verify) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…VATE_KEY first) and fail fast Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssumed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…DP, POA, KUBI Resume-safe (skips migrated orgs, idempotent seeds), verifies every proposal via the Winner(valid,executed) event, settles KUBI's stale proposals, banners the KUBI second-voter windows, stops hard with plain instructions on any failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…runs later via ORGS=KUBI Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urce — closes the founder-mint blind spot The two join-path events cannot see deploy-time founder mints (or admin/EM direct mints). Caught live post-cutover on Test6: the founder EOA — an explicitly-allowed, eligible Executive on the legacy EM — was silently not seeded (v2 Executive 4 vs legacy 5). The subgraph indexes every mint path since genesis; it is now the third candidate source. Founders verified present in all four fixtures (KUBI 23->24, Poa 7->10, DP 3->4 candidates). Repair for Test6: out/test6.repair.1.json (2 calls, run-proposal compatible). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ure message prints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rum 2, only two second-voter asks) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… + 11 seats, one governed batch - KubiRoles.s.sol: GenerateKubiRoles (writes out/kubi.roles.1.json, 13 calls) + SimKubiRoles (full governed migration replay + roles batch via authority-side creator, hard asserts on predicted subject ids, group derivation, post-cutover stamps; measured announce 3.89M < 5M) - migrate-kubi.sh: roles proposal joins round 2 (same window as cutover, announced cutover-FIRST), roles-only path for an already-migrated org, RPC fallback (gateway.fm storage flake) - RehearseMigration.s.sol: T5 shadow probe no longer requires per-row divergence — dual-hat wearers legitimately fold to the same word (KUBI: Member proj-2 subset of Executive global-175); aggregate shadowChecked>0 still enforces the per-project path (112 rows materialize live) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…round 2 start Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
CI note: the current Build & Test and Slither failures occur in the reusable workflow setup before any POP formatting, compilation, tests, or Slither command runs. The published Etherform SHA pins foundry-toolchain v1.8.0, which invokes the newly native foundryup executable through Bash and exits with cannot execute binary file. A rerun reproduced the same setup-only failure. Local production build, focused/full contract suites listed in the PR, formatting, shell syntax, fork replay, size, and gas checks passed. The durable fix is a narrow foundry-toolchain v1.9.1 update in Etherform followed by updating POP to its published Etherform SHA; I have intentionally not coupled this product PR to the unmerged broad dependency-refresh PR BreadchainCoop/etherform#58. |
|
Operational note from the final read-only audit: KUBI currently has proposals #30 (cutover) and #31 (board roles). Both closed with two votes and no Winner event; neither is finalized. MembershipAuthority remains paused. These proposals were created by a separate live migration session while this review was running, not by this PR workflow or its reviewers. No local broadcast-capable migration process is running now. Do not regenerate, vote, announce, or execute from stale local state without first reconciling these two on-chain proposals. |
Summary
Metadata correctness
Legacy EligibilityModule stores a nonzero metadata CID in Hats.details, so treating details as a role name can persist a raw digest. Migration now folds HatMetadataUpdated events into reviewed fixtures, rejects missing or drifting data, filters metadata pointers and legacy image sentinels, and uses the existing renameSubject path to preserve metadata without adding a migration-only ABI.
Already-seeded but paused migrations are repaired idempotently at the start of cutover. Native v2 deployment also persists role CID/image values while the authority remains paused. OrgDeployer now reports VERSION 2.0.0 to mark the breaking deployment ABI boundary.
Validation
Rollout notes