feat(TaskManager): project folders + organizer hat (v4 upgrade)#160
Open
hudsonhrh wants to merge 1 commit into
Open
feat(TaskManager): project folders + organizer hat (v4 upgrade)#160hudsonhrh wants to merge 1 commit into
hudsonhrh wants to merge 1 commit into
Conversation
Adds an IPFS-rooted folder tree for projects, gated by a configurable organizer hat. Folder structure (names, parents, ordering, project assignments) lives off-chain in a JSON document; only the root hash sits on-chain, swapped atomically via `setFolders(expectedRoot, newRoot)` with CAS-guarded concurrency. Strict permissions: executor or organizer hat only — creator hats deliberately do NOT inherit, to avoid silent reparenting of the whole tree by widely-distributed creator roles. Also: full NatSpec pass across TaskManager errors, events, and externals; 13 new folder tests + storage-preservation upgrade test (199 + 25 pass); v4 upgrade script with `DryRun_GnosisUpgrade` exercised against a live Gnosis fork. CLAUDE.md tightened to require Claude to run the sim (not punt to the user) and to pick `VERSION` by querying `getVersionCount` + `cast code` on every target chain instead of guessing. Follow-up issue #158 tracks splitting TaskManager into shared-storage libraries (mirror HybridVoting pattern). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hudsonhrh
added a commit
that referenced
this pull request
May 13, 2026
…s PR Both PRs upgrade TaskManager; ship them as a single v4 impl. The auto-merge combined cleanly except for two seams that needed manual review: 1. setConfig: PR #159 moved _requireExecutor() from the top of setConfig into each branch. PR #160's new ORGANIZER_HAT_ALLOWED branch was written under the old "gate at the top" assumption and so auto-merged WITHOUT an _requireExecutor() check — anyone could grant themselves the organizer hat. Added the missing check, plus a regression assertion in test_SetConfigOtherKeysStillExecutorOnly and a new check (5m) in the combined dry-run sim that locks down ORGANIZER_HAT_ALLOWED post-upgrade. 2. CLAUDE.md: kept both clarifications — production-profile-mandate rationale (#159) and ImplementationRegistry + CREATE3-slot VERSION probing (#160) — and merged the Stack-too-deep guidance so it's internally consistent with point 5. Upgrade scripts consolidated to a single UpgradeTaskManagerFolders.s.sol (v4). Its DryRun_GnosisUpgrade now also exercises the editable-budgets gate (pre/post Unauthorized vs NotExecutor on a real project, executor PROJECT_CAP / BOUNTY_CAP / ROLE_PERM regression checks, organizer-key executor-only assertion). UpgradeTaskManagerEditableBudgets.s.sol deleted — its checks now live inside the folders sim. - forge build: clean (default profile) - forge fmt: clean - forge test: 1330/1330 pass - FOUNDRY_PROFILE=production forge script ...:DryRun_GnosisUpgrade --fork-url gnosis: ALL CHECKS PASSED end-to-end on real Gnosis state Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 tasks
hudsonhrh
added a commit
that referenced
this pull request
May 13, 2026
Audit of the PR #159+#160 merge confirmed both features are intact, but neither the unit tests nor the dry-run sim explicitly proved a single hat-wearer could exercise the new hat-gated paths for BOTH features end-to-end on real upgraded bytecode. Closing that gap: - New unit test (`test_BudgetAndFolders_HatsAreIndependent`) creates a BUDGET hat-wearer and an organizer hat-wearer and proves they can each only do their own thing — BUDGET hat cannot setFolders (NotOrganizer), organizer hat cannot setConfig(PROJECT_CAP) (Unauthorized) — locking down the per-branch refactor against future cross-wiring. - New fork sim (`SimTaskManagerV4Integration.s.sol:Sim_HatWearerIntegration`) applies the v4 upgrade against a Gnosis org's live TaskManager, etches a controllable HatsShim over the org's real Hats address, grants a single test hat both TaskPerm.BUDGET and organizer status, and verifies on real upgraded bytecode that the hat-wearer can: - edit PROJECT_CAP - edit BOUNTY_CAP - setFolders (with CAS guard) - chain a follow-up setFolders …while still being rejected (NotExecutor) on all five admin keys, and a separate non-hat EOA is rejected with the correct Unauthorized / NotOrganizer reverts. Verified: - forge build clean (default + production profiles) - forge fmt clean - forge test --match-contract "TaskManager|UpgradeSafety" -> 233/233 pass - FOUNDRY_PROFILE=production forge script script/upgrades/SimTaskManagerV4Integration.s.sol:Sim_HatWearerIntegration --fork-url gnosis -> ALL HAT-WEARER INTEGRATION CHECKS PASSED Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hudsonhrh
added a commit
that referenced
this pull request
May 19, 2026
* feat(TaskManager): editable project budgets via TaskPerm.BUDGET hat (v3 upgrade) Lets a configured hat-holder — alongside the Executor (and therefore a passing vote) — resize a project's PT cap and per-token bounty cap. Permission is strict: project managers do not get implicit access, they need the hat too. - TaskPerm.BUDGET = 1 << 5 (new bit; no storage changes). - setConfig refactored from a single top-of-function _requireExecutor() into per-branch checks. PROJECT_CAP and BOUNTY_CAP use a new _requireBudgetEditor helper that skips the _isPM bypass; other admin keys (EXECUTOR, CREATOR_HAT_ALLOWED, ROLE_PERM, PROJECT_MANAGER) remain executor-only. - Grant: setConfig(ROLE_PERM, abi.encode(hat, TaskPerm.BUDGET)) globally, or setProjectRolePerm(pid, hat, TaskPerm.BUDGET) per-project. - Cross-chain v3 upgrade script (UpgradeTaskManagerEditableBudgets.s.sol) with DryRun_EditableBudgets sim that passes end-to-end on a Gnosis fork: pre-upgrade probe -> NotExecutor, post-upgrade probe -> Unauthorized, storage preserved, executor flow still works, other admin keys unchanged. - 8 new unit tests + updated test_SetBountyCapPermissions to the new revert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(TaskManager): project folders + organizer hat (v4 upgrade) Adds an IPFS-rooted folder tree for projects, gated by a configurable organizer hat. Folder structure (names, parents, ordering, project assignments) lives off-chain in a JSON document; only the root hash sits on-chain, swapped atomically via `setFolders(expectedRoot, newRoot)` with CAS-guarded concurrency. Strict permissions: executor or organizer hat only — creator hats deliberately do NOT inherit, to avoid silent reparenting of the whole tree by widely-distributed creator roles. Also: full NatSpec pass across TaskManager errors, events, and externals; 13 new folder tests + storage-preservation upgrade test (199 + 25 pass); v4 upgrade script with `DryRun_GnosisUpgrade` exercised against a live Gnosis fork. CLAUDE.md tightened to require Claude to run the sim (not punt to the user) and to pick `VERSION` by querying `getVersionCount` + `cast code` on every target chain instead of guessing. Follow-up issue #158 tracks splitting TaskManager into shared-storage libraries (mirror HybridVoting pattern). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(CLAUDE): require FOUNDRY_PROFILE=production for sims Default-profile sims deploy different bytecode than what broadcast actually sends (optimizer off, evm=osaka vs cancun). They are not real simulations. Make the production-profile requirement explicit instead of leaving it implicit in each script's docstring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(TaskManager): cross-feature hat-path integration test + fork sim Audit of the PR #159+#160 merge confirmed both features are intact, but neither the unit tests nor the dry-run sim explicitly proved a single hat-wearer could exercise the new hat-gated paths for BOTH features end-to-end on real upgraded bytecode. Closing that gap: - New unit test (`test_BudgetAndFolders_HatsAreIndependent`) creates a BUDGET hat-wearer and an organizer hat-wearer and proves they can each only do their own thing — BUDGET hat cannot setFolders (NotOrganizer), organizer hat cannot setConfig(PROJECT_CAP) (Unauthorized) — locking down the per-branch refactor against future cross-wiring. - New fork sim (`SimTaskManagerV4Integration.s.sol:Sim_HatWearerIntegration`) applies the v4 upgrade against a Gnosis org's live TaskManager, etches a controllable HatsShim over the org's real Hats address, grants a single test hat both TaskPerm.BUDGET and organizer status, and verifies on real upgraded bytecode that the hat-wearer can: - edit PROJECT_CAP - edit BOUNTY_CAP - setFolders (with CAS guard) - chain a follow-up setFolders …while still being rejected (NotExecutor) on all five admin keys, and a separate non-hat EOA is rejected with the correct Unauthorized / NotOrganizer reverts. Verified: - forge build clean (default + production profiles) - forge fmt clean - forge test --match-contract "TaskManager|UpgradeSafety" -> 233/233 pass - FOUNDRY_PROFILE=production forge script script/upgrades/SimTaskManagerV4Integration.s.sol:Sim_HatWearerIntegration --fork-url gnosis -> ALL HAT-WEARER INTEGRATION CHECKS PASSED Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: TaskManager folders spec (schema, CID encoding, pinning, CAS retry) Resolves the open question from PR #159 / issue #162: what is the off-chain JSON shape, who pins it, how do CIDs round-trip on/off chain, and how do clients handle the FoldersRootStale revert from concurrent edits. Decisions baked in: - Flat list of folder records with parentId pointers (not nested tree). Makes drag-drop edits a single-row delta and keeps CAS rebase tractable. - schemaVersion field with strict forward-incompat rule (newer = refuse to render) to keep evolution safe. - CID encoding identical to existing metadataHash convention (raw sha256 digest from CIDv0); no new encoding rules. - foldersRoot == bytes32(0) is reserved as "uninitialized / cleared", treated as the empty tree without IPFS resolution. - Pinning expectation: platform-managed via Poa-frontend/Poa-site backend. Contracts make no provision for hosting; clients coordinate off-chain — same convention already implicit in every other IPFS-anchored hash in the protocol. The spec is the authoritative reference for frontend, subgraph, and backend implementations. Unblocks Poa-frontend#399. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(TaskManager): emit RolePermSet on setConfig(ROLE_PERM) — closes #161 setConfig(ROLE_PERM, abi.encode(hatId, mask)) was the only path for granting TaskPerm.BUDGET globally that emitted no mask-carrying event. HatToggled fires from _syncPermissionHat, but that only signals "hat is tracked in permissionHatIds" — not what bits are set. This left the subgraph (and any indexer) unable to answer "which hats have BUDGET globally" without per-hat RPC fan-out. New event mirrors ProjectRolePermSet's shape minus the project id: event RolePermSet(uint256 indexed hatId, uint8 mask); Emitted on every ROLE_PERM update including mask=0 revokes, so indexers see clears without having to diff state. Verified: - forge build clean (default + production profiles) - forge fmt clean - forge test --match-contract "TaskManager|UpgradeSafety" -> 234/234 pass - FOUNDRY_PROFILE=production forge script script/upgrades/UpgradeTaskManagerFolders.s.sol:DryRun_GnosisUpgrade --fork-url gnosis -> ALL DRY-RUN CHECKS PASSED - FOUNDRY_PROFILE=production forge script script/upgrades/SimTaskManagerV4Integration.s.sol:Sim_HatWearerIntegration --fork-url gnosis -> ALL HAT-WEARER INTEGRATION CHECKS PASSED Unblocks subgraph indexing (poa-box/subgraph-pop#176). Closes #161 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(v4): pre-broadcast audit + paymaster fix + OrgDeployer bootstrap Three pre-broadcast considerations for v4 that were tracked in conversation but never landed in-tree: 1. **TaskPerm bit-5 collision audit** (script/audit/AuditTaskPermBit5.s.sol). v4 introduces TaskPerm.BUDGET = 1 << 5. Pre-v4, bit 5 was unused, but setConfig(ROLE_PERM, ...) accepts uint8 verbatim — any hat historically granted a mask with bit 5 set silently gains BUDGET post-upgrade. Script reads rolePermGlobal storage via vm.load on ERC-7201 slot (no public getter exists). Documented subgraph query covers the per-project surface (ProjectRolePermSet has been indexed since deploy). Verified against live state: 8 Gnosis orgs (Test, Test6, KUBI, ...) + Poa on Arbitrum + per-project subgraph query — all clean. Zero pre-existing bit-5 grants. Safe to broadcast. 2. **Retroactive paymaster fix** (script/fixes/AddSetFoldersSelectorRules.s.sol). Mirrors AddCreateTasksBatchSelectorRules.s.sol exactly. Adds setFolders selector (0x0c1b690e) to KUBI/Test6 (Gnosis) and Poa (Arbitrum) paymaster rules so organizer-hat wearers can publish folder updates gaslessly via 4337/passkey instead of bringing their own ETH. Both sims pass under FOUNDRY_PROFILE=production against real forks: Arbitrum/Poa: rule false -> true; Gnosis/KUBI + Test6: rule false -> true. Ready to broadcast once v4 lands. 3. **OrgDeployer bootstrap wiring** (src/OrgDeployer.sol). _appendTaskManagerRules now includes setFolders in the default paymaster whitelist for newly deployed orgs. Bumps the count comment from TaskManager(13) to TaskManager(14) and total from 40 to 41. Without this, every future org would need the same retroactive fix as KUBI/Poa/Test6. Verified: forge test full suite 1332/1332 pass (incl. DeployerTest which exercises the full org deployment + paymaster bootstrap path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * script(v4): end-to-end governance grant sim for KUBI/Test6/Poa Option B from the post-v4 rollout plan. For each live org, a real proposal that grants the new TaskManager v4 powers (TaskPerm.BUDGET + organizer slot) to the recommended role: KUBI (Gnosis) - Executive hat Test6 (Gnosis) - Executive hat Poa (Arbitrum) - CONTRIBUTOR hat This is sim-only — the production rollout is a member of each org submitting the same proposal via the Poa-frontend votes page. What this file proves is that the proposal payload is correct and the executor actually lands both setConfig calls when the proposal passes. Sim covers the FULL governance pipeline: 1. Apply v4 to the PoaManager beacon. 2. Etch a PermissiveHatsShim over the org's Hats Protocol address so a test voter passes both onlyCreator (createProposal) and class-hat (vote) checks without minting real hats. 3. Build the IExecutor.Call[] batch with both setConfig calls. 4. createProposal (10-min duration, single option). 5. Vote 100% for option 0. 6. vm.warp past endTimestamp. 7. announceWinner -> executor.execute fires inside. 8. Verify post-state: - rolePermGlobal[hat] has TaskPerm.BUDGET (bit 5) set - organizerHatIds contains the target hat All three sims PASS end-to-end under FOUNDRY_PROFILE=production against real production state: KUBI: v4 swap OK; proposal id 16; mask 0->32; orgs 0->1; valid=true Test6: v4 swap OK; proposal id 14; mask 0->32; orgs 0->1; valid=true Poa: v4 swap OK; proposal id 0; mask 0->32; orgs 0->1; valid=true (Proposal ids reflect each org's live history on the fork. KUBI is the most active; Poa has never voted yet on Arbitrum.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * script(v4): add Broadcast variants to GrantV4PermsViaGovernance The existing Sim* contracts proved the full propose -> vote -> execute pipeline works end-to-end on a fork. These new Broadcast* contracts actually create the proposal on-chain — useful right now since the Poa-frontend organizer-admin UI (PR #402) isn't merged yet, so submitting the proposal from the UI is friction-prone. Each Broadcast contract: 1. Reads PRIVATE_KEY / DEPLOYER_PRIVATE_KEY from env. 2. Sanity-checks the sender wears at least one creator hat on the org's HybridVoting (otherwise createProposal would revert NotCreator and burn gas). 3. Builds the same two-call batch the sim validated: - setConfig(ROLE_PERM, abi.encode(targetHat, TaskPerm.BUDGET)) - setConfig(ORGANIZER_HAT_ALLOWED, abi.encode(targetHat, true)) 4. Calls HybridVoting.createProposal with a 3-day (4320 minute) duration and a single option (unrestricted poll). 5. Logs the new proposal ID. Verified Hudson's wallet (the broadcaster) wears creator hats on all three orgs: KUBI -> Executive + Member (both are creator hats) Test6 -> Executive + Member (both are creator hats) Poa -> CONTRIBUTOR (also the creator hat) So the sanity-check guard will pass on every chain. Note: voting + announceWinner remain manual — members vote per their normal cadence; after the 3-day window expires, anyone calls announceWinner(id) and the executor lands both setConfig calls atomically. The Sim* contracts already proved that announceWinner -> executor.execute pipeline works against real production state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * script(v4): per-org proposal duration in BroadcastGrant* KUBI already broadcast at 4320 min (3 days). Test6 + Poa drop to 30 min since they're test/governance orgs and don't need a 3-day window. Constants split out near the bottom of the file so future tuning is a one-liner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * script(v4): OrgDeployer v12 upgrade for setFolders bootstrap Companion to UpgradeTaskManagerFolders (TaskManager v4 adds setFolders). Without this, every new org would need the same retroactive AddSetFoldersSelectorRules fix applied to KUBI/Test6/Poa — instead, this upgrade rewires the OrgDeployer bootstrap so future orgs auto-whitelist the setFolders selector on first deploy. Live impl: v11 (createTasksBatch bootstrap) This PR: v12 (setFolders bootstrap, in addition to createTasksBatch) Version selection: v9/v10/v11 taken on Gnosis (registered impls). v12 is FREE on both Gnosis and Arbitrum CREATE3 slots + ImplementationRegistry. Verified via the CLAUDE.md probing recipe before writing this file. Three-step cross-chain pattern (same as UpgradeOrgDeployerCreateTasksBatch): Step1_DeployImplOnGnosis — DD deploy on Gnosis Step2_UpgradeFromArbitrum — DD deploy on Arb + upgradeBeaconCrossChain Step3_Verify — read Gnosis impl after ~5 min relay Verification approach changed mid-implementation: an initial bytecode- pattern scan for the setFolders selector (0x0c1b690e) produced false negatives because Solidity pre-computes / inlines constants in ways that break a literal 4-byte search. Replaced with a real behavior assertion in test/DeployerTest.t.sol:testDeployFullOrgWithPaymasterAutoWhitelist — deploys a fresh org through the new OrgDeployer and asserts paymasterHub.getRule(orgId, taskManager, setFolders).allowed. Verified: - forge build clean (production profile) - forge test --match-test testDeployFullOrgWithPaymaster -> 3/3 pass - FOUNDRY_PROFILE=production forge script ...:SimulateOrgDeployerFoldersUpgrade --fork-url arbitrum -> "Arbitrum upgrade simulation: PASS" DD deploys v12 at 0xbC8510ca3F017FD889828242c77fb1f1b2FF88df (19093 bytes) and upgradeBeaconCrossChain dispatches successfully. Broadcast sequence (after TaskManager v4 lands): source .env && FOUNDRY_PROFILE=production forge script \ script/upgrades/UpgradeOrgDeployerFolders.s.sol:Step1_DeployImplOnGnosis \ --rpc-url gnosis --broadcast --slow source .env && FOUNDRY_PROFILE=production forge script \ script/upgrades/UpgradeOrgDeployerFolders.s.sol:Step2_UpgradeFromArbitrum \ --rpc-url arbitrum --broadcast --slow # wait ~5 min for Hyperlane relay, then: forge script script/upgrades/UpgradeOrgDeployerFolders.s.sol:Step3_Verify \ --rpc-url gnosis Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- 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
setFolders(expectedRoot, newRoot)lets an org publish an IPFS root for its folder tree (names, parents, ordering, project assignments live in JSON off-chain); CAS-guarded against silent concurrent overwrites; permission is strictly executor OR wearer of anyorganizerHatIdshat (configurable viasetConfig(ORGANIZER_HAT_ALLOWED, …)).forge fmt --checkclean.DryRun_GnosisUpgrade, run end-to-end against a live Gnosis fork (PASS) — v3 was rejected because its CREATE2 slot is occupied; v4 verified free on Gnosis and Arbitrum.VERSIONmust be picked by queryingImplementationRegistry.getVersionCount+cast codeon every target chain rather than guessing.Follow-up issue #158 tracks splitting TaskManager into shared-storage libraries (HybridVoting pattern).
Test plan
forge buildclean (default profile)forge test --match-contract TaskManagerFoldersTest -vvv— 13/13 passforge test --match-test testTaskManagerStoragePreservedAfterFoldersUpgrade -vvv— passforge test --match-path 'test/TaskManager.t.sol'— 199/199 passforge test --match-contract UpgradeSafetyTest— 25/25 passforge fmt --checkcleanFOUNDRY_PROFILE=production forge script ...:DryRun_GnosisUpgrade --rpc-url gnosis -vvv— PASS against live statepoa-box/subgraph-pop) to indexFoldersUpdated+OrganizerHatAllowed🤖 Generated with Claude Code