Skip to content

feat(TaskManager): project folders + organizer hat (v4 upgrade)#160

Open
hudsonhrh wants to merge 1 commit into
mainfrom
hudsonhrh/project-folders-ipfs
Open

feat(TaskManager): project folders + organizer hat (v4 upgrade)#160
hudsonhrh wants to merge 1 commit into
mainfrom
hudsonhrh/project-folders-ipfs

Conversation

@hudsonhrh
Copy link
Copy Markdown
Member

Summary

  • New featuresetFolders(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 any organizerHatIds hat (configurable via setConfig(ORGANIZER_HAT_ALLOWED, …)).
  • Quality pass — full NatSpec across TaskManager errors, events, and externals (including pre-existing gaps); 13 new folder tests + storage-preservation upgrade test (199 + 25 pass); forge fmt --check clean.
  • v4 upgrade script with 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.
  • CLAUDE.md tightened — Claude must run the sim itself (public RPC aliases need no auth), and VERSION must be picked by querying ImplementationRegistry.getVersionCount + cast code on every target chain rather than guessing.

Follow-up issue #158 tracks splitting TaskManager into shared-storage libraries (HybridVoting pattern).

Test plan

  • forge build clean (default profile)
  • forge test --match-contract TaskManagerFoldersTest -vvv — 13/13 pass
  • forge test --match-test testTaskManagerStoragePreservedAfterFoldersUpgrade -vvv — pass
  • forge test --match-path 'test/TaskManager.t.sol' — 199/199 pass
  • forge test --match-contract UpgradeSafetyTest — 25/25 pass
  • forge fmt --check clean
  • FOUNDRY_PROFILE=production forge script ...:DryRun_GnosisUpgrade --rpc-url gnosis -vvv — PASS against live state
  • Subgraph follow-up PR (poa-box/subgraph-pop) to index FoldersUpdated + OrganizerHatAllowed

🤖 Generated with Claude Code

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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant