feat(TaskManager v4): editable budgets + folders + organizer hat#159
Merged
Conversation
…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>
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>
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>
…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>
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>
This was referenced May 13, 2026
…try) 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>
…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>
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>
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>
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>
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>
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>
hudsonhrh
added a commit
that referenced
this pull request
Jun 1, 2026
… wins Reconciles main's TaskManager v4/v5 + EligibilityModule lockdown (#159, #167, #169) with the capability-hat refactor. Per direction, the capability-hat model is canonical and main's new features were re-expressed in it. Resolution highlights: - TaskManager Layout: kept main's foldersRoot/organizerHatIds in their on-chain slots (live-org upgrade safety), appended cap-hat gate fields after, plus new budgetHat/editMetaHat/editFullHat. Append-only vs deployed v5 layout. - v4 editable budgets: BOUNTY_CAP/PROJECT_CAP now gated by _requireBudgetEditor -> BUDGET capability hat (executor OR cap-hat), not the bitmask. - v5 post-claim edits: updateTask (EDIT_FULL) + updateTaskMetadata (EDIT_META) gate via _hasCap, not _permMask. - Folders kept verbatim (orthogonal; organizerHatIds stays a HatManager array gate). - bootstrapGlobalPerms re-expressed as a deployer-time bulk global gate-hat setter (expands each mask into per-gate cap-hat assignments); deleted the dead bitmask helpers (_permMask, _syncPermissionHat, refcount machinery). - setConfig(ROLE_PERM)/setProjectRolePerm extended to BUDGET/EDIT_META/EDIT_FULL, kept strict single-bit (InvalidCapMask). - EligibilityModule: kept #167 superAdmin-only lockdown; the sole exception is setWearerEligibility via onlySuperAdminOrRevoker (RoleBundleHatter cascade). - OrgDeployer: DeploymentParams keeps both capabilityHats/roleBundles and taskManagerPerms (complementary). Re-added _resolveRoleIndicesToHatIds. - Added lens variant 12 ([budgetHat, editMetaHat, editFullHat]) for the new gates. Security fix surfaced by the merge (pre-existing on the branch; DeployerTest was previously excluded from verification): RoleBundleHatter.mintRole's pre-mint eligibility reset unconditionally set wearers eligible, bypassing the vouching gate (defaults.eligible=false) on self-service QuickJoin. Now gated on hasSpecificWearerRules so it only re-enables previously-revoked wearers (preserves re-mint-after-revoke) and never overrides a default-driven gate for fresh wearers. Tests: 1423 passing / 0 failing / 16 skipped (obsolete bitmask tests). Main-era bitmask tests adapted to capability-hat behavioral assertions. forge build/fmt clean. Co-Authored-By: Claude Opus 4.8 (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
TaskManager v4 — a single upgrade shipping two features, plus the full rollout (cross-chain deploy script, paymaster retro-fix, OrgDeployer bootstrap update, governance grant scripts, on-chain audit, fork sims for every step).
Editable project budgets — new
TaskPerm.BUDGET = 1 << 5lets a configured hat-holder resize a project's PT cap + per-token bounty caps. Strict gating: no_isPMbypass; project managers need an explicit hat assignment. Implemented by movingsetConfig's top-of-function_requireExecutor()into each branch and adding a_requireBudgetEditor(pid)helper for the budget keys. Other admin keys stay executor-only.Project folders + organizer hat —
setFolders(expectedRoot, newRoot)publishes an IPFS root for the org's folder tree, CAS-guarded against concurrent writes. Gated on executor OR anyorganizerHatIdswearer (configurable viasetConfig(ORGANIZER_HAT_ALLOWED, ...)). Newbytes32 foldersRoot+uint256[] organizerHatIdsappended to Layout (storage-layout safe). Off-chain JSON schema fully spec'd atdocs/TASK_MANAGER_FOLDERS.md.Plus a small follow-up event —
setConfig(ROLE_PERM, ...)now emitsRolePermSet(hatId, mask)so the subgraph can answer "which hats have BUDGET globally" from logs alone (closes #161).What's in this PR
Contract changes
src/TaskManager.sol— BUDGET perm, folders, organizer hat, RolePermSet event, per-branchsetConfigpermissionssrc/libs/TaskPerm.sol— new BUDGET bitsrc/OrgDeployer.sol—_appendTaskManagerRulesincludes setFolders so future orgs auto-whitelisttest/TaskManager.t.sol— 8 BUDGET tests + folder tests + cross-feature integration testtest/UpgradeSafety.t.sol— storage-preservation testtest/DeployerTest.t.sol— asserts setFolders is in default paymaster rulesOff-chain spec —
docs/TASK_MANAGER_FOLDERS.md: JSON schema (flat list with parentId pointers +schemaVersion), CID encoding (same as existing metadataHash convention), pinning policy, CAS retry semantics.Scripts shipped
script/upgrades/UpgradeTaskManagerFolders.s.sol— v4 cross-chain upgrade (Step 1/2/3) +DryRun_GnosisUpgradescript/upgrades/UpgradeOrgDeployerFolders.s.sol— v12 OrgDeployer upgrade +SimulateOrgDeployerFoldersUpgradescript/upgrades/SimTaskManagerV4Integration.s.sol—Sim_HatWearerIntegration(etches a Hats shim to prove hat-wearer paths land on real bytecode)script/fixes/AddSetFoldersSelectorRules.s.sol— retro paymaster fix for KUBI/Test6/Poa + simsscript/audit/AuditTaskPermBit5.s.sol— pre-broadcast audit confirming no existing hat got BUDGET silentlyscript/fixes/GrantV4PermsViaGovernance.s.sol— full propose → vote → execute sim + per-org Broadcast variants (KUBI 4320 min, Test6/Poa 30 min)Process tightening
CLAUDE.md— production-profile is now mandatory for sims (default-profile sims deploy different bytecode than broadcast)Recommended hat grants per org
Each org's existing role tree, with the hat governance should grant
TaskPerm.BUDGET+ organizer status:Verification status
forge buildclean (default + production profiles)forge fmt --checkcleanforge test→ 1332/1332 pass (full suite, no regressions)FOUNDRY_PROFILE=production forge script …:DryRun_GnosisUpgrade --fork-url gnosis→ ALL CHECKS PASSEDFOUNDRY_PROFILE=production forge script …:Sim_HatWearerIntegration --fork-url gnosis→ ALL CHECKS PASSEDforge script …:AuditTaskPermBit5{Gnosis,ArbitrumDirect}→ 0 collisions across 9 live orgsFOUNDRY_PROFILE=production forge script …:SimAddSetFolders{Gnosis,Arbitrum} --fork-url …→ both PASSFOUNDRY_PROFILE=production forge script …:SimGrant{Kubi,Test6,Poa} --fork-url …→ full propose→vote→execute pipeline PASS for all threeFOUNDRY_PROFILE=production forge script …:SimulateOrgDeployerFoldersUpgrade --fork-url arbitrum→ PASSOn-chain rollout status (broadcast already in progress)
0xD1721E7Bb458C21485cbC7175A557C23Bb4be358on Gnosis + ArbitrumgetRule(...).allowed == true)0xbC8510ca3F017FD889828242c77fb1f1b2FF88dfon Gnosis (future orgs auto-whitelist setFolders)rolePermGlobal[hat] = 32AND is inorganizerHatIdsannounceWinnercall after voting window closesannounceWinnercall after voting window closesClosed by this PR
RolePermSetevent for global ROLE_PERM grants)🤖 Generated with Claude Code