From d617fb375e4136e5aeb8750ddab386ac40e23cf1 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Tue, 9 Dec 2025 16:38:11 -0500 Subject: [PATCH 1/9] feat: enable project/task creation during org deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow organizations to define initial projects and tasks in deployment config, created atomically in the same transaction as org deployment. Includes JSON parsing, role index resolution, and bootstrap infrastructure in TaskManager. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/openzeppelin-contracts-upgradeable | 2 +- script/DeployOrg.s.sol | 166 +++++++++++++++++++++++++ script/org-config-example.json | 36 +++++- src/OrgDeployer.sol | 86 +++++++++++++ src/TaskManager.sol | 103 ++++++++++++++- src/factories/ModulesFactory.sol | 2 +- src/libs/ModuleDeploymentLib.sol | 13 +- 7 files changed, 399 insertions(+), 9 deletions(-) diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 723f8ca..aa677e9 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 +Subproject commit aa677e9d28ed78fc427ec47ba2baef2030c58e7c diff --git a/script/DeployOrg.s.sol b/script/DeployOrg.s.sol index 01a2fae..41be421 100644 --- a/script/DeployOrg.s.sol +++ b/script/DeployOrg.s.sol @@ -40,6 +40,7 @@ contract DeployOrg is Script { address[] ddInitialTargets; bool withPaymaster; bool withEducationHub; // Whether to deploy EducationHub (default: true) + BootstrapConfigJson bootstrap; // Optional: initial projects and tasks } struct QuorumConfig { @@ -106,6 +107,33 @@ contract DeployOrg is Script { uint256[] ddCreatorRoles; } + // Bootstrap config structs for initial project/task creation + struct BootstrapProjectConfigJson { + string title; + bytes32 metadataHash; + uint256 cap; + address[] managers; + uint256[] createRoles; + uint256[] claimRoles; + uint256[] reviewRoles; + uint256[] assignRoles; + } + + struct BootstrapTaskConfigJson { + uint8 projectIndex; + uint256 payout; + string title; + bytes32 metadataHash; + address bountyToken; + uint256 bountyPayout; + bool requiresApplication; + } + + struct BootstrapConfigJson { + BootstrapProjectConfigJson[] projects; + BootstrapTaskConfigJson[] tasks; + } + /*═══════════════════════════ MAIN DEPLOYMENT ═══════════════════════════*/ function run() public { @@ -325,9 +353,101 @@ contract DeployOrg is Script { bytes memory ddTargetsData = vm.parseJson(configJson, ".ddInitialTargets"); config.ddInitialTargets = abi.decode(ddTargetsData, (address[])); + // Parse bootstrap config (optional) + config.bootstrap = _parseBootstrapConfig(configJson); + return config; } + function _parseBootstrapConfig(string memory configJson) internal returns (BootstrapConfigJson memory bootstrap) { + // Count bootstrap projects + uint256 projectsLength = 0; + for (uint256 i = 0; i < 100; i++) { + try vm.parseJsonString(configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title")) returns ( + string memory + ) { + projectsLength++; + } catch { + break; + } + } + + if (projectsLength == 0) { + return bootstrap; // No bootstrap config + } + + bootstrap.projects = new BootstrapProjectConfigJson[](projectsLength); + for (uint256 i = 0; i < projectsLength; i++) { + string memory basePath = string.concat(".bootstrap.projects[", vm.toString(i), "]"); + + bootstrap.projects[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); + + // Parse metadataHash (optional, default to 0) + try vm.parseJsonBytes32(configJson, string.concat(basePath, ".metadataHash")) returns (bytes32 hash) { + bootstrap.projects[i].metadataHash = hash; + } catch { + bootstrap.projects[i].metadataHash = bytes32(0); + } + + bootstrap.projects[i].cap = vm.parseJsonUint(configJson, string.concat(basePath, ".cap")); + + // Parse managers array (optional) + try vm.parseJson(configJson, string.concat(basePath, ".managers")) returns (bytes memory managersData) { + bootstrap.projects[i].managers = abi.decode(managersData, (address[])); + } catch { + bootstrap.projects[i].managers = new address[](0); + } + + // Parse role arrays + bytes memory createRolesData = vm.parseJson(configJson, string.concat(basePath, ".createRoles")); + bootstrap.projects[i].createRoles = abi.decode(createRolesData, (uint256[])); + + bytes memory claimRolesData = vm.parseJson(configJson, string.concat(basePath, ".claimRoles")); + bootstrap.projects[i].claimRoles = abi.decode(claimRolesData, (uint256[])); + + bytes memory reviewRolesData = vm.parseJson(configJson, string.concat(basePath, ".reviewRoles")); + bootstrap.projects[i].reviewRoles = abi.decode(reviewRolesData, (uint256[])); + + bytes memory assignRolesData = vm.parseJson(configJson, string.concat(basePath, ".assignRoles")); + bootstrap.projects[i].assignRoles = abi.decode(assignRolesData, (uint256[])); + } + + // Count bootstrap tasks + uint256 tasksLength = 0; + for (uint256 i = 0; i < 100; i++) { + try vm.parseJsonString(configJson, string.concat(".bootstrap.tasks[", vm.toString(i), "].title")) returns ( + string memory + ) { + tasksLength++; + } catch { + break; + } + } + + bootstrap.tasks = new BootstrapTaskConfigJson[](tasksLength); + for (uint256 i = 0; i < tasksLength; i++) { + string memory basePath = string.concat(".bootstrap.tasks[", vm.toString(i), "]"); + + bootstrap.tasks[i].projectIndex = uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); + bootstrap.tasks[i].payout = vm.parseJsonUint(configJson, string.concat(basePath, ".payout")); + bootstrap.tasks[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); + + // Parse metadataHash (optional, default to 0) + try vm.parseJsonBytes32(configJson, string.concat(basePath, ".metadataHash")) returns (bytes32 hash) { + bootstrap.tasks[i].metadataHash = hash; + } catch { + bootstrap.tasks[i].metadataHash = bytes32(0); + } + + bootstrap.tasks[i].bountyToken = vm.parseJsonAddress(configJson, string.concat(basePath, ".bountyToken")); + bootstrap.tasks[i].bountyPayout = vm.parseJsonUint(configJson, string.concat(basePath, ".bountyPayout")); + bootstrap.tasks[i].requiresApplication = + vm.parseJsonBool(configJson, string.concat(basePath, ".requiresApplication")); + } + + return bootstrap; + } + /*═══════════════════════════ PARAM BUILDING ═══════════════════════════*/ /** @@ -433,9 +553,55 @@ contract DeployOrg is Script { // Build education hub config params.educationHubConfig = ModulesFactory.EducationHubConfig({enabled: config.withEducationHub}); + // Build bootstrap config for initial projects/tasks + params.bootstrap = _buildBootstrapConfig(config.bootstrap); + return params; } + function _buildBootstrapConfig(BootstrapConfigJson memory bootstrapJson) + internal + pure + returns (OrgDeployer.BootstrapConfig memory bootstrap) + { + if (bootstrapJson.projects.length == 0) { + return bootstrap; // Empty bootstrap + } + + // Build project configs + // Note: Role indices will be converted to hat IDs by OrgDeployer at deployment time + bootstrap.projects = new OrgDeployer.ITaskManagerBootstrap.BootstrapProjectConfig[](bootstrapJson.projects.length); + for (uint256 i = 0; i < bootstrapJson.projects.length; i++) { + bootstrap.projects[i] = OrgDeployer.ITaskManagerBootstrap.BootstrapProjectConfig({ + title: bytes(bootstrapJson.projects[i].title), + metadataHash: bootstrapJson.projects[i].metadataHash, + cap: bootstrapJson.projects[i].cap, + managers: bootstrapJson.projects[i].managers, + // Note: These are role indices, OrgDeployer will resolve to hat IDs + createHats: bootstrapJson.projects[i].createRoles, + claimHats: bootstrapJson.projects[i].claimRoles, + reviewHats: bootstrapJson.projects[i].reviewRoles, + assignHats: bootstrapJson.projects[i].assignRoles + }); + } + + // Build task configs + bootstrap.tasks = new OrgDeployer.ITaskManagerBootstrap.BootstrapTaskConfig[](bootstrapJson.tasks.length); + for (uint256 i = 0; i < bootstrapJson.tasks.length; i++) { + bootstrap.tasks[i] = OrgDeployer.ITaskManagerBootstrap.BootstrapTaskConfig({ + projectIndex: bootstrapJson.tasks[i].projectIndex, + payout: bootstrapJson.tasks[i].payout, + title: bytes(bootstrapJson.tasks[i].title), + metadataHash: bootstrapJson.tasks[i].metadataHash, + bountyToken: bootstrapJson.tasks[i].bountyToken, + bountyPayout: bootstrapJson.tasks[i].bountyPayout, + requiresApplication: bootstrapJson.tasks[i].requiresApplication + }); + } + + return bootstrap; + } + /*═══════════════════════════ OUTPUT ═══════════════════════════*/ function _outputDeployment(OrgConfigJson memory config, OrgDeployer.DeploymentResult memory result) internal view { diff --git a/script/org-config-example.json b/script/org-config-example.json index bc1dee0..60e093f 100644 --- a/script/org-config-example.json +++ b/script/org-config-example.json @@ -54,5 +54,39 @@ }, "ddInitialTargets": [], "withPaymaster": false, - "withEducationHub": true + "withEducationHub": true, + "bootstrap": { + "projects": [ + { + "title": "Getting Started", + "metadataHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "cap": "1000000000000000000000", + "managers": [], + "createRoles": [1], + "claimRoles": [0, 1, 2], + "reviewRoles": [1], + "assignRoles": [1] + } + ], + "tasks": [ + { + "projectIndex": 0, + "title": "Complete your profile", + "metadataHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "payout": "10000000000000000000", + "bountyToken": "0x0000000000000000000000000000000000000000", + "bountyPayout": "0", + "requiresApplication": false + }, + { + "projectIndex": 0, + "title": "Introduce yourself in the community", + "metadataHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "payout": "5000000000000000000", + "bountyToken": "0x0000000000000000000000000000000000000000", + "bountyPayout": "0", + "requiresApplication": false + } + ] + } } diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index 2489876..d7efcb5 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -43,6 +43,34 @@ interface IPaymasterHub { function registerOrg(bytes32 orgId, uint256 adminHatId, uint256 operatorHatId) external; } +interface ITaskManagerBootstrap { + struct BootstrapProjectConfig { + bytes title; + bytes32 metadataHash; + uint256 cap; + address[] managers; + uint256[] createHats; + uint256[] claimHats; + uint256[] reviewHats; + uint256[] assignHats; + } + + struct BootstrapTaskConfig { + uint8 projectIndex; + uint256 payout; + bytes title; + bytes32 metadataHash; + address bountyToken; + uint256 bountyPayout; + bool requiresApplication; + } + + function bootstrapProjectsAndTasks( + BootstrapProjectConfig[] calldata projects, + BootstrapTaskConfig[] calldata tasks + ) external returns (bytes32[] memory projectIds); +} + /** * @title OrgDeployer * @notice Thin orchestrator for deploying complete organizations using factory pattern @@ -175,6 +203,11 @@ contract OrgDeployer is Initializable { uint256 ddCreatorRolesBitmap; // Bit N set = Role N can create polls } + struct BootstrapConfig { + ITaskManagerBootstrap.BootstrapProjectConfig[] projects; + ITaskManagerBootstrap.BootstrapTaskConfig[] tasks; + } + struct DeploymentParams { bytes32 orgId; string orgName; @@ -191,6 +224,7 @@ contract OrgDeployer is Initializable { RoleAssignments roleAssignments; bool passkeyEnabled; // Whether passkey support is enabled (uses universal factory) ModulesFactory.EducationHubConfig educationHubConfig; // EducationHub deployment configuration + BootstrapConfig bootstrap; // Optional: initial projects and tasks to create } /*════════════════ VALIDATION ════════════════*/ @@ -369,6 +403,16 @@ contract OrgDeployer is Initializable { IParticipationToken(result.participationToken).setEducationHub(result.educationHub); } + /* 8.5. Bootstrap initial projects and tasks if configured */ + if (params.bootstrap.projects.length > 0) { + // Resolve role indices to hat IDs in bootstrap config + ITaskManagerBootstrap.BootstrapProjectConfig[] memory resolvedProjects = + _resolveBootstrapRoles(params.bootstrap.projects, gov.roleHatIds); + ITaskManagerBootstrap(result.taskManager).bootstrapProjectsAndTasks( + resolvedProjects, params.bootstrap.tasks + ); + } + /* 9. Authorize QuickJoin to mint hats */ IExecutorAdmin(result.executor).setHatMinterAuthorization(result.quickJoin, true); @@ -585,4 +629,46 @@ contract OrgDeployer is Initializable { // Forward batch registration to OrgRegistry (we are the owner) l.orgRegistry.batchRegisterOrgContracts(orgId, registrations, autoUpgrade, lastRegister); } + + /** + * @notice Resolve role indices to hat IDs in bootstrap project configs + * @dev Role indices in config are converted to actual hat IDs using roleHatIds array + */ + function _resolveBootstrapRoles( + ITaskManagerBootstrap.BootstrapProjectConfig[] calldata projects, + uint256[] memory roleHatIds + ) internal pure returns (ITaskManagerBootstrap.BootstrapProjectConfig[] memory resolved) { + resolved = new ITaskManagerBootstrap.BootstrapProjectConfig[](projects.length); + + for (uint256 i = 0; i < projects.length; i++) { + resolved[i] = ITaskManagerBootstrap.BootstrapProjectConfig({ + title: projects[i].title, + metadataHash: projects[i].metadataHash, + cap: projects[i].cap, + managers: projects[i].managers, + createHats: _resolveRoleIndicesToHatIds(projects[i].createHats, roleHatIds), + claimHats: _resolveRoleIndicesToHatIds(projects[i].claimHats, roleHatIds), + reviewHats: _resolveRoleIndicesToHatIds(projects[i].reviewHats, roleHatIds), + assignHats: _resolveRoleIndicesToHatIds(projects[i].assignHats, roleHatIds) + }); + } + + return resolved; + } + + /** + * @notice Convert array of role indices to array of hat IDs + */ + function _resolveRoleIndicesToHatIds(uint256[] calldata roleIndices, uint256[] memory roleHatIds) + internal + pure + returns (uint256[] memory hatIds) + { + hatIds = new uint256[](roleIndices.length); + for (uint256 i = 0; i < roleIndices.length; i++) { + require(roleIndices[i] < roleHatIds.length, "Invalid role index in bootstrap config"); + hatIds[i] = roleHatIds[roleIndices[i]]; + } + return hatIds; + } } diff --git a/src/TaskManager.sol b/src/TaskManager.sol index aeb64ec..698c8e2 100644 --- a/src/TaskManager.sol +++ b/src/TaskManager.sol @@ -34,6 +34,7 @@ contract TaskManager is Initializable, ContextUpgradeable { error NotCreator(); error NotClaimer(); error NotExecutor(); + error NotDeployer(); error Unauthorized(); error NotApplicant(); error AlreadyApplied(); @@ -86,6 +87,28 @@ contract TaskManager is Initializable, ContextUpgradeable { mapping(address => BudgetLib.Budget) bountyBudgets; // slot 2: rest of slot & beyond } + /*──────── Bootstrap Config Structs ───────*/ + struct BootstrapProjectConfig { + bytes title; + bytes32 metadataHash; + uint256 cap; + address[] managers; + uint256[] createHats; + uint256[] claimHats; + uint256[] reviewHats; + uint256[] assignHats; + } + + struct BootstrapTaskConfig { + uint8 projectIndex; // References project in same batch (0 for first project) + uint256 payout; + bytes title; + bytes32 metadataHash; + address bountyToken; + uint256 bountyPayout; + bool requiresApplication; + } + /*──────── Storage (ERC-7201) ───────*/ struct Layout { mapping(bytes32 => Project) _projects; @@ -101,6 +124,7 @@ contract TaskManager is Initializable, ContextUpgradeable { uint256[] permissionHatIds; // enumeration array for hats with permissions mapping(uint256 => address[]) taskApplicants; // task ID => array of applicants mapping(uint256 => mapping(address => bytes32)) taskApplications; // task ID => applicant => application hash + address deployer; // OrgDeployer address for bootstrap operations } bytes32 private constant _STORAGE_SLOT = 0x30bc214cbc65463577eb5b42c88d60986e26fc81ad89a2eb74550fb255f1e712; @@ -147,7 +171,8 @@ contract TaskManager is Initializable, ContextUpgradeable { address tokenAddress, address hatsAddress, uint256[] calldata creatorHats, - address executorAddress + address executorAddress, + address deployerAddress ) external initializer { tokenAddress.requireNonZeroAddress(); hatsAddress.requireNonZeroAddress(); @@ -159,6 +184,7 @@ contract TaskManager is Initializable, ContextUpgradeable { l.token = IParticipationToken(tokenAddress); l.hats = IHats(hatsAddress); l.executor = executorAddress; + l.deployer = deployerAddress; // Can be address(0) if bootstrap not needed // Initialize creator hat arrays using HatManager for (uint256 i; i < creatorHats.length;) { @@ -220,6 +246,22 @@ contract TaskManager is Initializable, ContextUpgradeable { uint256[] calldata assignHats ) external returns (bytes32 projectId) { _requireCreator(); + projectId = _createProjectInternal( + title, metadataHash, cap, managers, createHats, claimHats, reviewHats, assignHats, _msgSender() + ); + } + + function _createProjectInternal( + bytes calldata title, + bytes32 metadataHash, + uint256 cap, + address[] calldata managers, + uint256[] calldata createHats, + uint256[] calldata claimHats, + uint256[] calldata reviewHats, + uint256[] calldata assignHats, + address defaultManager + ) internal returns (bytes32 projectId) { ValidationLib.requireValidTitle(title); ValidationLib.requireValidCapAmount(cap); @@ -230,8 +272,10 @@ contract TaskManager is Initializable, ContextUpgradeable { p.exists = true; /* managers */ - p.managers[_msgSender()] = true; - emit ProjectManagerUpdated(projectId, _msgSender(), true); + if (defaultManager != address(0)) { + p.managers[defaultManager] = true; + emit ProjectManagerUpdated(projectId, defaultManager, true); + } for (uint256 i; i < managers.length;) { managers[i].requireNonZeroAddress(); p.managers[managers[i]] = true; @@ -260,6 +304,59 @@ contract TaskManager is Initializable, ContextUpgradeable { emit ProjectDeleted(pid); } + /** + * @notice Bootstrap initial projects and tasks during org deployment + * @dev Only callable by deployer (OrgDeployer) during bootstrap phase + * @param projects Array of project configurations to create + * @param tasks Array of task configurations (reference projects by index) + * @return projectIds Array of created project IDs + */ + function bootstrapProjectsAndTasks( + BootstrapProjectConfig[] calldata projects, + BootstrapTaskConfig[] calldata tasks + ) external returns (bytes32[] memory projectIds) { + Layout storage l = _layout(); + if (_msgSender() != l.deployer) revert NotDeployer(); + + projectIds = new bytes32[](projects.length); + + // Create all projects (executor is not auto-added as manager, use managers array) + for (uint256 i; i < projects.length;) { + projectIds[i] = _createProjectInternal( + projects[i].title, + projects[i].metadataHash, + projects[i].cap, + projects[i].managers, + projects[i].createHats, + projects[i].claimHats, + projects[i].reviewHats, + projects[i].assignHats, + address(0) // No default manager - use explicit managers array + ); + unchecked { + ++i; + } + } + + // Create all tasks referencing projects by index + for (uint256 i; i < tasks.length;) { + if (tasks[i].projectIndex >= projects.length) revert InvalidIndex(); + bytes32 pid = projectIds[tasks[i].projectIndex]; + _createTask( + tasks[i].payout, + tasks[i].title, + tasks[i].metadataHash, + pid, + tasks[i].requiresApplication, + tasks[i].bountyToken, + tasks[i].bountyPayout + ); + unchecked { + ++i; + } + } + } + /*──────── Task Logic ───────*/ function createTask( uint256 payout, diff --git a/src/factories/ModulesFactory.sol b/src/factories/ModulesFactory.sol index 4942452..c26b616 100644 --- a/src/factories/ModulesFactory.sol +++ b/src/factories/ModulesFactory.sol @@ -105,7 +105,7 @@ contract ModulesFactory { }); result.taskManager = ModuleDeploymentLib.deployTaskManager( - config, params.executor, params.participationToken, creatorHats, taskManagerBeacon + config, params.executor, params.participationToken, creatorHats, taskManagerBeacon, params.deployer ); } diff --git a/src/libs/ModuleDeploymentLib.sol b/src/libs/ModuleDeploymentLib.sol index 2b26ef0..ecf13d2 100644 --- a/src/libs/ModuleDeploymentLib.sol +++ b/src/libs/ModuleDeploymentLib.sol @@ -63,7 +63,13 @@ interface IParticipationTokenInit { } interface ITaskManagerInit { - function initialize(address token, address hats, uint256[] calldata creatorHats, address executor) external; + function initialize( + address token, + address hats, + uint256[] calldata creatorHats, + address executor, + address deployer + ) external; } interface IEducationHubInit { @@ -194,10 +200,11 @@ library ModuleDeploymentLib { address executorAddr, address token, uint256[] memory creatorHats, - address beacon + address beacon, + address deployer ) internal returns (address tmProxy) { bytes memory init = abi.encodeWithSelector( - ITaskManagerInit.initialize.selector, token, config.hats, creatorHats, executorAddr + ITaskManagerInit.initialize.selector, token, config.hats, creatorHats, executorAddr, deployer ); tmProxy = deployCore(config, ModuleTypes.TASK_MANAGER_ID, init, beacon); } From dec2a339cb4a6377ec8124ac116dc1c499714b25 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Tue, 9 Dec 2025 17:42:43 -0500 Subject: [PATCH 2/9] test: add comprehensive bootstrap test suite and fix deploy scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 10 bootstrap tests to TaskManager.t.sol covering: - Single/multiple projects, managers, role permissions - Bounty tasks, application-required tasks - Invalid inputs, empty arrays, full task lifecycle - Add bootstrap integration support to DeployerTest.t.sol - Add _emptyBootstrap() helper function - Update all 18 DeploymentParams to include bootstrap field - Fix deploy script type references: - Import ITaskManagerBootstrap from OrgDeployer.sol - Change OrgDeployer.ITaskManagerBootstrap -> ITaskManagerBootstrap - Add bootstrap parsing/building to RunOrgActions.s.sol and RunOrgActionsAdvanced.s.sol for full deploy script support - Pin OpenZeppelin submodule to v5.0.2 (correct version for codebase) All 557 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/openzeppelin-contracts-upgradeable | 2 +- script/DeployOrg.s.sol | 10 +- script/RunOrgActions.s.sol | 168 ++++++++++- script/RunOrgActionsAdvanced.s.sol | 168 ++++++++++- test/DeployerTest.t.sol | 63 ++-- test/TaskManager.t.sol | 392 ++++++++++++++++++++++++- 6 files changed, 775 insertions(+), 28 deletions(-) diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index aa677e9..723f8ca 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit aa677e9d28ed78fc427ec47ba2baef2030c58e7c +Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 diff --git a/script/DeployOrg.s.sol b/script/DeployOrg.s.sol index 41be421..b6c8994 100644 --- a/script/DeployOrg.s.sol +++ b/script/DeployOrg.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import "forge-std/Script.sol"; import "forge-std/console.sol"; -import {OrgDeployer} from "../src/OrgDeployer.sol"; +import {OrgDeployer, ITaskManagerBootstrap} from "../src/OrgDeployer.sol"; import {IHybridVotingInit} from "../src/libs/ModuleDeploymentLib.sol"; import {RoleConfigStructs} from "../src/libs/RoleConfigStructs.sol"; import {ModulesFactory} from "../src/factories/ModulesFactory.sol"; @@ -570,9 +570,9 @@ contract DeployOrg is Script { // Build project configs // Note: Role indices will be converted to hat IDs by OrgDeployer at deployment time - bootstrap.projects = new OrgDeployer.ITaskManagerBootstrap.BootstrapProjectConfig[](bootstrapJson.projects.length); + bootstrap.projects = new ITaskManagerBootstrap.BootstrapProjectConfig[](bootstrapJson.projects.length); for (uint256 i = 0; i < bootstrapJson.projects.length; i++) { - bootstrap.projects[i] = OrgDeployer.ITaskManagerBootstrap.BootstrapProjectConfig({ + bootstrap.projects[i] = ITaskManagerBootstrap.BootstrapProjectConfig({ title: bytes(bootstrapJson.projects[i].title), metadataHash: bootstrapJson.projects[i].metadataHash, cap: bootstrapJson.projects[i].cap, @@ -586,9 +586,9 @@ contract DeployOrg is Script { } // Build task configs - bootstrap.tasks = new OrgDeployer.ITaskManagerBootstrap.BootstrapTaskConfig[](bootstrapJson.tasks.length); + bootstrap.tasks = new ITaskManagerBootstrap.BootstrapTaskConfig[](bootstrapJson.tasks.length); for (uint256 i = 0; i < bootstrapJson.tasks.length; i++) { - bootstrap.tasks[i] = OrgDeployer.ITaskManagerBootstrap.BootstrapTaskConfig({ + bootstrap.tasks[i] = ITaskManagerBootstrap.BootstrapTaskConfig({ projectIndex: bootstrapJson.tasks[i].projectIndex, payout: bootstrapJson.tasks[i].payout, title: bytes(bootstrapJson.tasks[i].title), diff --git a/script/RunOrgActions.s.sol b/script/RunOrgActions.s.sol index 50f67d0..edcdee7 100644 --- a/script/RunOrgActions.s.sol +++ b/script/RunOrgActions.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import "forge-std/Script.sol"; import "forge-std/console.sol"; -import {OrgDeployer} from "../src/OrgDeployer.sol"; +import {OrgDeployer, ITaskManagerBootstrap} from "../src/OrgDeployer.sol"; import {TaskManager} from "../src/TaskManager.sol"; import {HybridVoting} from "../src/HybridVoting.sol"; import {ParticipationToken} from "../src/ParticipationToken.sol"; @@ -63,6 +63,7 @@ contract RunOrgActions is Script { address[] ddInitialTargets; bool withPaymaster; bool withEducationHub; // Whether to deploy EducationHub (default: true) + BootstrapConfigJson bootstrap; // Optional: initial projects and tasks } struct QuorumConfig { @@ -129,6 +130,33 @@ contract RunOrgActions is Script { uint256[] ddCreatorRoles; } + // Bootstrap config structs for initial project/task creation + struct BootstrapProjectConfigJson { + string title; + bytes32 metadataHash; + uint256 cap; + address[] managers; + uint256[] createRoles; + uint256[] claimRoles; + uint256[] reviewRoles; + uint256[] assignRoles; + } + + struct BootstrapTaskConfigJson { + uint8 projectIndex; + uint256 payout; + string title; + bytes32 metadataHash; + address bountyToken; + uint256 bountyPayout; + bool requiresApplication; + } + + struct BootstrapConfigJson { + BootstrapProjectConfigJson[] projects; + BootstrapTaskConfigJson[] tasks; + } + struct OrgContracts { address executor; address hybridVoting; @@ -736,9 +764,101 @@ contract RunOrgActions is Script { bytes memory ddTargetsData = vm.parseJson(configJson, ".ddInitialTargets"); config.ddInitialTargets = abi.decode(ddTargetsData, (address[])); + // Parse bootstrap config (optional) + config.bootstrap = _parseBootstrapConfig(configJson); + return config; } + function _parseBootstrapConfig(string memory configJson) internal returns (BootstrapConfigJson memory bootstrap) { + // Count bootstrap projects + uint256 projectsLength = 0; + for (uint256 i = 0; i < 100; i++) { + try vm.parseJsonString(configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title")) returns ( + string memory + ) { + projectsLength++; + } catch { + break; + } + } + + if (projectsLength == 0) { + return bootstrap; // No bootstrap config + } + + bootstrap.projects = new BootstrapProjectConfigJson[](projectsLength); + for (uint256 i = 0; i < projectsLength; i++) { + string memory basePath = string.concat(".bootstrap.projects[", vm.toString(i), "]"); + + bootstrap.projects[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); + + // Parse metadataHash (optional, default to 0) + try vm.parseJsonBytes32(configJson, string.concat(basePath, ".metadataHash")) returns (bytes32 hash) { + bootstrap.projects[i].metadataHash = hash; + } catch { + bootstrap.projects[i].metadataHash = bytes32(0); + } + + bootstrap.projects[i].cap = vm.parseJsonUint(configJson, string.concat(basePath, ".cap")); + + // Parse managers array (optional) + try vm.parseJson(configJson, string.concat(basePath, ".managers")) returns (bytes memory managersData) { + bootstrap.projects[i].managers = abi.decode(managersData, (address[])); + } catch { + bootstrap.projects[i].managers = new address[](0); + } + + // Parse role arrays + bytes memory createRolesData = vm.parseJson(configJson, string.concat(basePath, ".createRoles")); + bootstrap.projects[i].createRoles = abi.decode(createRolesData, (uint256[])); + + bytes memory claimRolesData = vm.parseJson(configJson, string.concat(basePath, ".claimRoles")); + bootstrap.projects[i].claimRoles = abi.decode(claimRolesData, (uint256[])); + + bytes memory reviewRolesData = vm.parseJson(configJson, string.concat(basePath, ".reviewRoles")); + bootstrap.projects[i].reviewRoles = abi.decode(reviewRolesData, (uint256[])); + + bytes memory assignRolesData = vm.parseJson(configJson, string.concat(basePath, ".assignRoles")); + bootstrap.projects[i].assignRoles = abi.decode(assignRolesData, (uint256[])); + } + + // Count bootstrap tasks + uint256 tasksLength = 0; + for (uint256 i = 0; i < 100; i++) { + try vm.parseJsonString(configJson, string.concat(".bootstrap.tasks[", vm.toString(i), "].title")) returns ( + string memory + ) { + tasksLength++; + } catch { + break; + } + } + + bootstrap.tasks = new BootstrapTaskConfigJson[](tasksLength); + for (uint256 i = 0; i < tasksLength; i++) { + string memory basePath = string.concat(".bootstrap.tasks[", vm.toString(i), "]"); + + bootstrap.tasks[i].projectIndex = uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); + bootstrap.tasks[i].payout = vm.parseJsonUint(configJson, string.concat(basePath, ".payout")); + bootstrap.tasks[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); + + // Parse metadataHash (optional, default to 0) + try vm.parseJsonBytes32(configJson, string.concat(basePath, ".metadataHash")) returns (bytes32 hash) { + bootstrap.tasks[i].metadataHash = hash; + } catch { + bootstrap.tasks[i].metadataHash = bytes32(0); + } + + bootstrap.tasks[i].bountyToken = vm.parseJsonAddress(configJson, string.concat(basePath, ".bountyToken")); + bootstrap.tasks[i].bountyPayout = vm.parseJsonUint(configJson, string.concat(basePath, ".bountyPayout")); + bootstrap.tasks[i].requiresApplication = + vm.parseJsonBool(configJson, string.concat(basePath, ".requiresApplication")); + } + + return bootstrap; + } + /*=========================== PARAM BUILDING ===========================*/ function _roleArrayToBitmap(uint256[] memory roles) internal pure returns (uint256 bitmap) { @@ -838,6 +958,52 @@ contract RunOrgActions is Script { // Build education hub config params.educationHubConfig = ModulesFactory.EducationHubConfig({enabled: config.withEducationHub}); + // Build bootstrap config for initial projects/tasks + params.bootstrap = _buildBootstrapConfig(config.bootstrap); + return params; } + + function _buildBootstrapConfig(BootstrapConfigJson memory bootstrapJson) + internal + pure + returns (OrgDeployer.BootstrapConfig memory bootstrap) + { + if (bootstrapJson.projects.length == 0) { + return bootstrap; // Empty bootstrap + } + + // Build project configs + // Note: Role indices will be converted to hat IDs by OrgDeployer at deployment time + bootstrap.projects = new ITaskManagerBootstrap.BootstrapProjectConfig[](bootstrapJson.projects.length); + for (uint256 i = 0; i < bootstrapJson.projects.length; i++) { + bootstrap.projects[i] = ITaskManagerBootstrap.BootstrapProjectConfig({ + title: bytes(bootstrapJson.projects[i].title), + metadataHash: bootstrapJson.projects[i].metadataHash, + cap: bootstrapJson.projects[i].cap, + managers: bootstrapJson.projects[i].managers, + // Note: These are role indices, OrgDeployer will resolve to hat IDs + createHats: bootstrapJson.projects[i].createRoles, + claimHats: bootstrapJson.projects[i].claimRoles, + reviewHats: bootstrapJson.projects[i].reviewRoles, + assignHats: bootstrapJson.projects[i].assignRoles + }); + } + + // Build task configs + bootstrap.tasks = new ITaskManagerBootstrap.BootstrapTaskConfig[](bootstrapJson.tasks.length); + for (uint256 i = 0; i < bootstrapJson.tasks.length; i++) { + bootstrap.tasks[i] = ITaskManagerBootstrap.BootstrapTaskConfig({ + projectIndex: bootstrapJson.tasks[i].projectIndex, + payout: bootstrapJson.tasks[i].payout, + title: bytes(bootstrapJson.tasks[i].title), + metadataHash: bootstrapJson.tasks[i].metadataHash, + bountyToken: bootstrapJson.tasks[i].bountyToken, + bountyPayout: bootstrapJson.tasks[i].bountyPayout, + requiresApplication: bootstrapJson.tasks[i].requiresApplication + }); + } + + return bootstrap; + } } diff --git a/script/RunOrgActionsAdvanced.s.sol b/script/RunOrgActionsAdvanced.s.sol index 52886c3..e005d20 100644 --- a/script/RunOrgActionsAdvanced.s.sol +++ b/script/RunOrgActionsAdvanced.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import "forge-std/Script.sol"; import "forge-std/console.sol"; -import {OrgDeployer} from "../src/OrgDeployer.sol"; +import {OrgDeployer, ITaskManagerBootstrap} from "../src/OrgDeployer.sol"; import {TaskManager} from "../src/TaskManager.sol"; import {HybridVoting} from "../src/HybridVoting.sol"; import {ParticipationToken} from "../src/ParticipationToken.sol"; @@ -67,6 +67,7 @@ contract RunOrgActionsAdvanced is Script { address[] ddInitialTargets; bool withPaymaster; bool withEducationHub; // Whether to deploy EducationHub (default: true) + BootstrapConfigJson bootstrap; // Optional: initial projects and tasks } struct QuorumConfig { @@ -133,6 +134,33 @@ contract RunOrgActionsAdvanced is Script { uint256[] ddCreatorRoles; } + // Bootstrap config structs for initial project/task creation + struct BootstrapProjectConfigJson { + string title; + bytes32 metadataHash; + uint256 cap; + address[] managers; + uint256[] createRoles; + uint256[] claimRoles; + uint256[] reviewRoles; + uint256[] assignRoles; + } + + struct BootstrapTaskConfigJson { + uint8 projectIndex; + uint256 payout; + string title; + bytes32 metadataHash; + address bountyToken; + uint256 bountyPayout; + bool requiresApplication; + } + + struct BootstrapConfigJson { + BootstrapProjectConfigJson[] projects; + BootstrapTaskConfigJson[] tasks; + } + struct OrgContracts { address executor; address hybridVoting; @@ -853,9 +881,101 @@ contract RunOrgActionsAdvanced is Script { bytes memory ddTargetsData = vm.parseJson(configJson, ".ddInitialTargets"); config.ddInitialTargets = abi.decode(ddTargetsData, (address[])); + // Parse bootstrap config (optional) + config.bootstrap = _parseBootstrapConfig(configJson); + return config; } + function _parseBootstrapConfig(string memory configJson) internal returns (BootstrapConfigJson memory bootstrap) { + // Count bootstrap projects + uint256 projectsLength = 0; + for (uint256 i = 0; i < 100; i++) { + try vm.parseJsonString(configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title")) returns ( + string memory + ) { + projectsLength++; + } catch { + break; + } + } + + if (projectsLength == 0) { + return bootstrap; // No bootstrap config + } + + bootstrap.projects = new BootstrapProjectConfigJson[](projectsLength); + for (uint256 i = 0; i < projectsLength; i++) { + string memory basePath = string.concat(".bootstrap.projects[", vm.toString(i), "]"); + + bootstrap.projects[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); + + // Parse metadataHash (optional, default to 0) + try vm.parseJsonBytes32(configJson, string.concat(basePath, ".metadataHash")) returns (bytes32 hash) { + bootstrap.projects[i].metadataHash = hash; + } catch { + bootstrap.projects[i].metadataHash = bytes32(0); + } + + bootstrap.projects[i].cap = vm.parseJsonUint(configJson, string.concat(basePath, ".cap")); + + // Parse managers array (optional) + try vm.parseJson(configJson, string.concat(basePath, ".managers")) returns (bytes memory managersData) { + bootstrap.projects[i].managers = abi.decode(managersData, (address[])); + } catch { + bootstrap.projects[i].managers = new address[](0); + } + + // Parse role arrays + bytes memory createRolesData = vm.parseJson(configJson, string.concat(basePath, ".createRoles")); + bootstrap.projects[i].createRoles = abi.decode(createRolesData, (uint256[])); + + bytes memory claimRolesData = vm.parseJson(configJson, string.concat(basePath, ".claimRoles")); + bootstrap.projects[i].claimRoles = abi.decode(claimRolesData, (uint256[])); + + bytes memory reviewRolesData = vm.parseJson(configJson, string.concat(basePath, ".reviewRoles")); + bootstrap.projects[i].reviewRoles = abi.decode(reviewRolesData, (uint256[])); + + bytes memory assignRolesData = vm.parseJson(configJson, string.concat(basePath, ".assignRoles")); + bootstrap.projects[i].assignRoles = abi.decode(assignRolesData, (uint256[])); + } + + // Count bootstrap tasks + uint256 tasksLength = 0; + for (uint256 i = 0; i < 100; i++) { + try vm.parseJsonString(configJson, string.concat(".bootstrap.tasks[", vm.toString(i), "].title")) returns ( + string memory + ) { + tasksLength++; + } catch { + break; + } + } + + bootstrap.tasks = new BootstrapTaskConfigJson[](tasksLength); + for (uint256 i = 0; i < tasksLength; i++) { + string memory basePath = string.concat(".bootstrap.tasks[", vm.toString(i), "]"); + + bootstrap.tasks[i].projectIndex = uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); + bootstrap.tasks[i].payout = vm.parseJsonUint(configJson, string.concat(basePath, ".payout")); + bootstrap.tasks[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); + + // Parse metadataHash (optional, default to 0) + try vm.parseJsonBytes32(configJson, string.concat(basePath, ".metadataHash")) returns (bytes32 hash) { + bootstrap.tasks[i].metadataHash = hash; + } catch { + bootstrap.tasks[i].metadataHash = bytes32(0); + } + + bootstrap.tasks[i].bountyToken = vm.parseJsonAddress(configJson, string.concat(basePath, ".bountyToken")); + bootstrap.tasks[i].bountyPayout = vm.parseJsonUint(configJson, string.concat(basePath, ".bountyPayout")); + bootstrap.tasks[i].requiresApplication = + vm.parseJsonBool(configJson, string.concat(basePath, ".requiresApplication")); + } + + return bootstrap; + } + /*=========================== PARAM BUILDING ===========================*/ function _roleArrayToBitmap(uint256[] memory roles) internal pure returns (uint256 bitmap) { @@ -955,6 +1075,52 @@ contract RunOrgActionsAdvanced is Script { // Build education hub config params.educationHubConfig = ModulesFactory.EducationHubConfig({enabled: config.withEducationHub}); + // Build bootstrap config for initial projects/tasks + params.bootstrap = _buildBootstrapConfig(config.bootstrap); + return params; } + + function _buildBootstrapConfig(BootstrapConfigJson memory bootstrapJson) + internal + pure + returns (OrgDeployer.BootstrapConfig memory bootstrap) + { + if (bootstrapJson.projects.length == 0) { + return bootstrap; // Empty bootstrap + } + + // Build project configs + // Note: Role indices will be converted to hat IDs by OrgDeployer at deployment time + bootstrap.projects = new ITaskManagerBootstrap.BootstrapProjectConfig[](bootstrapJson.projects.length); + for (uint256 i = 0; i < bootstrapJson.projects.length; i++) { + bootstrap.projects[i] = ITaskManagerBootstrap.BootstrapProjectConfig({ + title: bytes(bootstrapJson.projects[i].title), + metadataHash: bootstrapJson.projects[i].metadataHash, + cap: bootstrapJson.projects[i].cap, + managers: bootstrapJson.projects[i].managers, + // Note: These are role indices, OrgDeployer will resolve to hat IDs + createHats: bootstrapJson.projects[i].createRoles, + claimHats: bootstrapJson.projects[i].claimRoles, + reviewHats: bootstrapJson.projects[i].reviewRoles, + assignHats: bootstrapJson.projects[i].assignRoles + }); + } + + // Build task configs + bootstrap.tasks = new ITaskManagerBootstrap.BootstrapTaskConfig[](bootstrapJson.tasks.length); + for (uint256 i = 0; i < bootstrapJson.tasks.length; i++) { + bootstrap.tasks[i] = ITaskManagerBootstrap.BootstrapTaskConfig({ + projectIndex: bootstrapJson.tasks[i].projectIndex, + payout: bootstrapJson.tasks[i].payout, + title: bytes(bootstrapJson.tasks[i].title), + metadataHash: bootstrapJson.tasks[i].metadataHash, + bountyToken: bootstrapJson.tasks[i].bountyToken, + bountyPayout: bootstrapJson.tasks[i].bountyPayout, + requiresApplication: bootstrapJson.tasks[i].requiresApplication + }); + } + + return bootstrap; + } } diff --git a/test/DeployerTest.t.sol b/test/DeployerTest.t.sol index 8a03afd..8da57be 100644 --- a/test/DeployerTest.t.sol +++ b/test/DeployerTest.t.sol @@ -25,7 +25,7 @@ import {UniversalAccountRegistry} from "../src/UniversalAccountRegistry.sol"; import "../src/ImplementationRegistry.sol"; import "../src/PoaManager.sol"; import "../src/OrgRegistry.sol"; -import {OrgDeployer} from "../src/OrgDeployer.sol"; +import {OrgDeployer, ITaskManagerBootstrap} from "../src/OrgDeployer.sol"; import {GovernanceFactory} from "../src/factories/GovernanceFactory.sol"; import {AccessFactory} from "../src/factories/AccessFactory.sol"; import {RoleConfigStructs} from "../src/libs/RoleConfigStructs.sol"; @@ -167,7 +167,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -247,6 +248,13 @@ contract DeployerTest is Test, IEligibilityModuleEvents { }); } + /// @dev Helper to build empty bootstrap config + function _emptyBootstrap() internal pure returns (OrgDeployer.BootstrapConfig memory) { + ITaskManagerBootstrap.BootstrapProjectConfig[] memory projects = new ITaskManagerBootstrap.BootstrapProjectConfig[](0); + ITaskManagerBootstrap.BootstrapTaskConfig[] memory tasks = new ITaskManagerBootstrap.BootstrapTaskConfig[](0); + return OrgDeployer.BootstrapConfig({projects: projects, tasks: tasks}); + } + /// @dev Helper to build legacy-style voting classes function _buildLegacyClasses(uint8 ddSplit, uint8 ptSplit, bool quadratic, uint256 minBal) internal @@ -347,7 +355,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -402,7 +411,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -728,7 +738,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -857,7 +868,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); deployer.deployFullOrg(params); @@ -895,7 +907,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -976,7 +989,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -1244,7 +1258,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -1456,7 +1471,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -1616,7 +1632,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -1750,7 +1767,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: emptyRoles, roleAssignments: _buildDefaultRoleAssignments(), passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); vm.expectRevert(OrgDeployer.InvalidRoleConfiguration.selector); @@ -2023,7 +2041,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -2126,7 +2145,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -2290,7 +2310,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -3271,7 +3292,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: roles, roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _emptyBootstrap() }); // Record logs to verify HatCreatedWithEligibility events were emitted @@ -3369,7 +3391,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: false}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: false}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -3428,7 +3451,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: false}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: false}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); @@ -3489,7 +3513,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { roles: _buildSimpleRoleConfigs(names, images, voting), roleAssignments: roleAssignments, passkeyEnabled: false, - educationHubConfig: ModulesFactory.EducationHubConfig({enabled: false}) + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: false}), + bootstrap: _emptyBootstrap() }); OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); diff --git a/test/TaskManager.t.sol b/test/TaskManager.t.sol index 6608933..c7b6c2a 100644 --- a/test/TaskManager.t.sol +++ b/test/TaskManager.t.sol @@ -159,7 +159,7 @@ contract MockToken is Test, IERC20 { uint256[] memory creatorHats = _hatArr(CREATOR_HAT); vm.prank(creator1); - tm.initialize(address(token), address(hats), creatorHats, executor); + tm.initialize(address(token), address(hats), creatorHats, executor, address(0)); vm.prank(executor); tm.setConfig( @@ -4842,3 +4842,393 @@ contract MockToken is Test, IERC20 { assertEq(spent2, 0, "Token2 spent should be rolled back"); } } + +/*────────────────── Bootstrap Test Suite ──────────────────*/ +contract TaskManagerBootstrapTest is Test { + /* test actors */ + address creator1 = makeAddr("creator1"); + address pm1 = makeAddr("pm1"); + address member1 = makeAddr("member1"); + address executor = makeAddr("executor"); + address deployer = makeAddr("deployer"); + address outsider = makeAddr("outsider"); + + uint256 constant CREATOR_HAT = 1; + uint256 constant PM_HAT = 2; + uint256 constant MEMBER_HAT = 3; + + TaskManager tm; + TaskManagerLens lens; + MockToken token; + MockHats hats; + MockERC20 bountyToken; + + function setUp() public { + token = new MockToken(); + hats = new MockHats(); + bountyToken = new MockERC20(); + + hats.mintHat(CREATOR_HAT, creator1); + hats.mintHat(PM_HAT, pm1); + hats.mintHat(MEMBER_HAT, member1); + + tm = new TaskManager(); + lens = new TaskManagerLens(); + uint256[] memory creatorHats = new uint256[](1); + creatorHats[0] = CREATOR_HAT; + + // Initialize with deployer address + tm.initialize(address(token), address(hats), creatorHats, executor, deployer); + + // Set up permissions + vm.prank(executor); + tm.setConfig( + TaskManager.ConfigKey.ROLE_PERM, + abi.encode(PM_HAT, TaskPerm.CREATE | TaskPerm.REVIEW | TaskPerm.ASSIGN) + ); + vm.prank(executor); + tm.setConfig(TaskManager.ConfigKey.ROLE_PERM, abi.encode(MEMBER_HAT, TaskPerm.CLAIM)); + } + + /* ─────────────── Helper Functions ─────────────── */ + + function _hatArr(uint256 hat) internal pure returns (uint256[] memory arr) { + arr = new uint256[](1); + arr[0] = hat; + } + + function _buildBootstrapProject( + string memory title, + uint256 cap, + uint256[] memory createHats, + uint256[] memory claimHats, + uint256[] memory reviewHats, + uint256[] memory assignHats + ) internal pure returns (TaskManager.BootstrapProjectConfig memory) { + return TaskManager.BootstrapProjectConfig({ + title: bytes(title), + metadataHash: bytes32(0), + cap: cap, + managers: new address[](0), + createHats: createHats, + claimHats: claimHats, + reviewHats: reviewHats, + assignHats: assignHats + }); + } + + function _buildBootstrapTask( + uint8 projectIndex, + string memory title, + uint256 payout + ) internal pure returns (TaskManager.BootstrapTaskConfig memory) { + return TaskManager.BootstrapTaskConfig({ + projectIndex: projectIndex, + payout: payout, + title: bytes(title), + metadataHash: bytes32(0), + bountyToken: address(0), + bountyPayout: 0, + requiresApplication: false + }); + } + + /* ─────────────── Test Cases ─────────────── */ + + function test_BootstrapSingleProjectWithTasks() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Getting Started", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](2); + tasks[0] = _buildBootstrapTask(0, "Complete your profile", 10 ether); + tasks[1] = _buildBootstrapTask(0, "Introduce yourself", 5 ether); + + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + + // Verify project created + assertEq(projectIds.length, 1, "Should create 1 project"); + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[0])); + (uint256 cap, uint256 spent, bool isManager) = abi.decode(result, (uint256, uint256, bool)); + assertEq(cap, 100 ether, "Project cap should be 100 ether"); + assertEq(spent, 15 ether, "Project spent should be 15 ether (both tasks)"); + + // Verify tasks created + result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); + (uint256 payout, TaskManager.Status status,,, bool requiresApp) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(payout, 10 ether, "Task 0 payout should be 10 ether"); + assertEq(uint8(status), uint8(TaskManager.Status.UNCLAIMED), "Task 0 should be UNCLAIMED"); + assertFalse(requiresApp, "Task 0 should not require application"); + + result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(1)); + (payout,,,, ) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(payout, 5 ether, "Task 1 payout should be 5 ether"); + } + + function test_BootstrapMultipleProjects() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](2); + projects[0] = _buildBootstrapProject("Project A", 50 ether, _hatArr(CREATOR_HAT), _hatArr(MEMBER_HAT), _hatArr(PM_HAT), _hatArr(PM_HAT)); + projects[1] = _buildBootstrapProject("Project B", 100 ether, _hatArr(CREATOR_HAT), _hatArr(MEMBER_HAT), _hatArr(PM_HAT), _hatArr(PM_HAT)); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](3); + tasks[0] = _buildBootstrapTask(0, "Task for A", 10 ether); + tasks[1] = _buildBootstrapTask(1, "Task 1 for B", 20 ether); + tasks[2] = _buildBootstrapTask(1, "Task 2 for B", 30 ether); + + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + + assertEq(projectIds.length, 2, "Should create 2 projects"); + + // Verify Project A spent + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[0])); + (, uint256 spentA,) = abi.decode(result, (uint256, uint256, bool)); + assertEq(spentA, 10 ether, "Project A spent should be 10 ether"); + + // Verify Project B spent + result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[1])); + (, uint256 spentB,) = abi.decode(result, (uint256, uint256, bool)); + assertEq(spentB, 50 ether, "Project B spent should be 50 ether"); + } + + function test_BootstrapWithManagers() public { + address[] memory managers = new address[](1); + managers[0] = pm1; + + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = TaskManager.BootstrapProjectConfig({ + title: bytes("Managed Project"), + metadataHash: bytes32(0), + cap: 100 ether, + managers: managers, + createHats: _hatArr(CREATOR_HAT), + claimHats: _hatArr(MEMBER_HAT), + reviewHats: _hatArr(PM_HAT), + assignHats: _hatArr(PM_HAT) + }); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + + // Expect the ProjectManagerUpdated event to be emitted + vm.expectEmit(true, true, false, true); + emit TaskManager.ProjectManagerUpdated(bytes32(0), pm1, true); + + vm.prank(deployer); + tm.bootstrapProjectsAndTasks(projects, tasks); + } + + function test_BootstrapWithRolePermissions() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Role Test Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = _buildBootstrapTask(0, "Claimable task", 10 ether); + + vm.prank(deployer); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // member1 has MEMBER_HAT which is in claimHats, should be able to claim + vm.prank(member1); + tm.claimTask(0); + + // Verify claim + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); + (, TaskManager.Status status, address claimer,,) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(claimer, member1, "member1 should be claimer"); + assertEq(uint8(status), uint8(TaskManager.Status.CLAIMED), "Task should be claimed"); + } + + function test_BootstrapWithBountyTasks() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Bounty Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = TaskManager.BootstrapTaskConfig({ + projectIndex: 0, + payout: 10 ether, + title: bytes("Bounty task"), + metadataHash: bytes32(0), + bountyToken: address(bountyToken), + bountyPayout: 5 ether, + requiresApplication: false + }); + + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + + // Verify bounty info stored + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_FULL_INFO, abi.encode(0)); + ( + uint256 payout, + uint256 bountyPayoutVal, + address bountyTokenAddr, + , + , + , + + ) = abi.decode(result, (uint256, uint256, address, TaskManager.Status, address, bytes32, bool)); + assertEq(payout, 10 ether, "Payout should be 10 ether"); + assertEq(bountyPayoutVal, 5 ether, "Bounty payout should be 5 ether"); + assertEq(bountyTokenAddr, address(bountyToken), "Bounty token should match"); + } + + function test_BootstrapWithRequiresApplication() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Application Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = TaskManager.BootstrapTaskConfig({ + projectIndex: 0, + payout: 10 ether, + title: bytes("Apply first"), + metadataHash: bytes32(0), + bountyToken: address(0), + bountyPayout: 0, + requiresApplication: true + }); + + vm.prank(deployer); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // Direct claim should fail + vm.prank(member1); + vm.expectRevert(TaskManager.RequiresApplication.selector); + tm.claimTask(0); + + // Apply and then get approved + vm.prank(member1); + tm.applyForTask(0, keccak256("my application")); + + vm.prank(pm1); + tm.approveApplication(0, member1); + + // Now claim should work + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); + (, TaskManager.Status status, address claimer,,) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(claimer, member1, "member1 should be claimer after approval"); + } + + function test_BootstrapOnlyDeployer() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Test Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + + // creator1 should not be able to bootstrap + vm.prank(creator1); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // executor should not be able to bootstrap + vm.prank(executor); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // outsider should not be able to bootstrap + vm.prank(outsider); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + } + + function test_BootstrapInvalidProjectIndex() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Only Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = _buildBootstrapTask(5, "Invalid reference", 10 ether); // projectIndex 5 doesn't exist + + vm.prank(deployer); + vm.expectRevert(TaskManager.InvalidIndex.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + } + + function test_BootstrapEmptyArrays() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](0); + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + + assertEq(projectIds.length, 0, "Should return empty array for empty bootstrap"); + } + + function test_BootstrapTaskLifecycleAfterBootstrap() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Lifecycle Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = _buildBootstrapTask(0, "Complete me", 10 ether); + + vm.prank(deployer); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // Full lifecycle: claim → submit → complete + uint256 balBefore = token.balanceOf(member1); + + vm.prank(member1); + tm.claimTask(0); + + vm.prank(member1); + tm.submitTask(0, keccak256("my work")); + + vm.prank(pm1); + tm.completeTask(0); + + // Verify minting + assertEq(token.balanceOf(member1), balBefore + 10 ether, "Should mint participation tokens"); + + // Verify task completed + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); + (, TaskManager.Status status,,,) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(uint8(status), uint8(TaskManager.Status.COMPLETED), "Task should be completed"); + } +} From d6469a69c5ed8809a6bd0280997de59f48374cee Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Fri, 23 Jan 2026 16:04:53 -0500 Subject: [PATCH 3/9] refactor: make deployer eligible only for roles they receive Deploy deployer eligibility is now conditional on `canVote && mintToDeployer`, matching minting behavior. Executor remains eligible for all roles as needed for governance operations. This prevents unnecessary eligibility entries and ensures role config intent is fully respected. - Only set deployer eligibility when they're receiving the hat - Executor stays eligible for all roles (required for QuickJoin) - Update CLAUDE.md to document that upgrades/ is auto-generated by CI - All 547 tests pass Co-Authored-By: Claude Haiku 4.5 --- CLAUDE.md | 1 + src/HatsTreeSetup.sol | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 38b8cb1..2c84169 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,7 @@ ## Rules - Do NOT run `foundryup` - Foundry is pre-installed - ALWAYS run `forge fmt` before commits and PRs +- Do NOT edit `upgrades/` folder - auto-generated by CI ## Project Solidity smart contracts for decentralized organizations (Foundry/Solidity ^0.8.20). diff --git a/src/HatsTreeSetup.sol b/src/HatsTreeSetup.sol index 2e1b04c..48ed03d 100644 --- a/src/HatsTreeSetup.sol +++ b/src/HatsTreeSetup.sol @@ -161,10 +161,14 @@ contract HatsTreeSetup { ); // Step 5: Collect all eligibility and toggle operations for batch execution - // Count total eligibility entries needed: 2 per role (executor + deployer) + additional wearers + // Count total eligibility entries needed: executor (always) + deployer (only if minting) + additional wearers uint256 eligibilityCount = 0; for (uint256 i = 0; i < len; i++) { - eligibilityCount += 2; // executor + deployer + eligibilityCount += 1; // executor always eligible + // Deployer only eligible if they're receiving the hat (matches minting conditions) + if (params.roles[i].canVote && params.roles[i].distribution.mintToDeployer) { + eligibilityCount += 1; + } eligibilityCount += params.roles[i].distribution.additionalWearers.length; } @@ -186,14 +190,17 @@ contract HatsTreeSetup { uint256 hatId = result.roleHatIds[i]; RoleConfigStructs.RoleConfig memory role = params.roles[i]; - // Collect eligibility entries + // Executor always eligible (needed for QuickJoin and governance operations) eligWearers[eligIndex] = params.executor; eligHatIds[eligIndex] = hatId; eligIndex++; - eligWearers[eligIndex] = params.deployerAddress; - eligHatIds[eligIndex] = hatId; - eligIndex++; + // Deployer only eligible if they're receiving the hat (matches minting conditions) + if (role.canVote && role.distribution.mintToDeployer) { + eligWearers[eligIndex] = params.deployerAddress; + eligHatIds[eligIndex] = hatId; + eligIndex++; + } // Collect additional wearers for (uint256 j = 0; j < role.distribution.additionalWearers.length; j++) { From bb32c5221ac7ec1ae18227c1de05590d841ed2bb Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Fri, 23 Jan 2026 16:20:07 -0500 Subject: [PATCH 4/9] style: fix formatting for PR 77 merged code Run forge fmt on merged code from org-deploy-project-task branch. Co-Authored-By: Claude Haiku 4.5 --- script/DeployOrg.s.sol | 7 +- script/RunOrgActions.s.sol | 7 +- script/RunOrgActionsAdvanced.s.sol | 7 +- src/OrgDeployer.sol | 12 +- src/TaskManager.sol | 8 +- src/libs/ModuleDeploymentLib.sol | 9 +- test/DeployerTest.t.sol | 3 +- test/TaskManager.t.sol | 727 +++++++++++++++-------------- 8 files changed, 395 insertions(+), 385 deletions(-) diff --git a/script/DeployOrg.s.sol b/script/DeployOrg.s.sol index bb1fdcc..8cf4726 100644 --- a/script/DeployOrg.s.sol +++ b/script/DeployOrg.s.sol @@ -363,7 +363,9 @@ contract DeployOrg is Script { // Count bootstrap projects uint256 projectsLength = 0; for (uint256 i = 0; i < 100; i++) { - try vm.parseJsonString(configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title")) returns ( + try vm.parseJsonString( + configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title") + ) returns ( string memory ) { projectsLength++; @@ -428,7 +430,8 @@ contract DeployOrg is Script { for (uint256 i = 0; i < tasksLength; i++) { string memory basePath = string.concat(".bootstrap.tasks[", vm.toString(i), "]"); - bootstrap.tasks[i].projectIndex = uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); + bootstrap.tasks[i].projectIndex = + uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); bootstrap.tasks[i].payout = vm.parseJsonUint(configJson, string.concat(basePath, ".payout")); bootstrap.tasks[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); diff --git a/script/RunOrgActions.s.sol b/script/RunOrgActions.s.sol index 6122c9e..379463d 100644 --- a/script/RunOrgActions.s.sol +++ b/script/RunOrgActions.s.sol @@ -774,7 +774,9 @@ contract RunOrgActions is Script { // Count bootstrap projects uint256 projectsLength = 0; for (uint256 i = 0; i < 100; i++) { - try vm.parseJsonString(configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title")) returns ( + try vm.parseJsonString( + configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title") + ) returns ( string memory ) { projectsLength++; @@ -839,7 +841,8 @@ contract RunOrgActions is Script { for (uint256 i = 0; i < tasksLength; i++) { string memory basePath = string.concat(".bootstrap.tasks[", vm.toString(i), "]"); - bootstrap.tasks[i].projectIndex = uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); + bootstrap.tasks[i].projectIndex = + uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); bootstrap.tasks[i].payout = vm.parseJsonUint(configJson, string.concat(basePath, ".payout")); bootstrap.tasks[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); diff --git a/script/RunOrgActionsAdvanced.s.sol b/script/RunOrgActionsAdvanced.s.sol index 5366192..c618806 100644 --- a/script/RunOrgActionsAdvanced.s.sol +++ b/script/RunOrgActionsAdvanced.s.sol @@ -891,7 +891,9 @@ contract RunOrgActionsAdvanced is Script { // Count bootstrap projects uint256 projectsLength = 0; for (uint256 i = 0; i < 100; i++) { - try vm.parseJsonString(configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title")) returns ( + try vm.parseJsonString( + configJson, string.concat(".bootstrap.projects[", vm.toString(i), "].title") + ) returns ( string memory ) { projectsLength++; @@ -956,7 +958,8 @@ contract RunOrgActionsAdvanced is Script { for (uint256 i = 0; i < tasksLength; i++) { string memory basePath = string.concat(".bootstrap.tasks[", vm.toString(i), "]"); - bootstrap.tasks[i].projectIndex = uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); + bootstrap.tasks[i].projectIndex = + uint8(vm.parseJsonUint(configJson, string.concat(basePath, ".projectIndex"))); bootstrap.tasks[i].payout = vm.parseJsonUint(configJson, string.concat(basePath, ".payout")); bootstrap.tasks[i].title = vm.parseJsonString(configJson, string.concat(basePath, ".title")); diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index 991461e..2f6a985 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -65,10 +65,9 @@ interface ITaskManagerBootstrap { bool requiresApplication; } - function bootstrapProjectsAndTasks( - BootstrapProjectConfig[] calldata projects, - BootstrapTaskConfig[] calldata tasks - ) external returns (bytes32[] memory projectIds); + function bootstrapProjectsAndTasks(BootstrapProjectConfig[] calldata projects, BootstrapTaskConfig[] calldata tasks) + external + returns (bytes32[] memory projectIds); } /** @@ -408,9 +407,8 @@ contract OrgDeployer is Initializable { // Resolve role indices to hat IDs in bootstrap config ITaskManagerBootstrap.BootstrapProjectConfig[] memory resolvedProjects = _resolveBootstrapRoles(params.bootstrap.projects, gov.roleHatIds); - ITaskManagerBootstrap(result.taskManager).bootstrapProjectsAndTasks( - resolvedProjects, params.bootstrap.tasks - ); + ITaskManagerBootstrap(result.taskManager) + .bootstrapProjectsAndTasks(resolvedProjects, params.bootstrap.tasks); } /* 9. Authorize QuickJoin to mint hats */ diff --git a/src/TaskManager.sol b/src/TaskManager.sol index 5e6e1ad..e675ef2 100644 --- a/src/TaskManager.sol +++ b/src/TaskManager.sol @@ -311,10 +311,10 @@ contract TaskManager is Initializable, ContextUpgradeable { * @param tasks Array of task configurations (reference projects by index) * @return projectIds Array of created project IDs */ - function bootstrapProjectsAndTasks( - BootstrapProjectConfig[] calldata projects, - BootstrapTaskConfig[] calldata tasks - ) external returns (bytes32[] memory projectIds) { + function bootstrapProjectsAndTasks(BootstrapProjectConfig[] calldata projects, BootstrapTaskConfig[] calldata tasks) + external + returns (bytes32[] memory projectIds) + { Layout storage l = _layout(); if (_msgSender() != l.deployer) revert NotDeployer(); diff --git a/src/libs/ModuleDeploymentLib.sol b/src/libs/ModuleDeploymentLib.sol index 667a78f..acb8082 100644 --- a/src/libs/ModuleDeploymentLib.sol +++ b/src/libs/ModuleDeploymentLib.sol @@ -63,13 +63,8 @@ interface IParticipationTokenInit { } interface ITaskManagerInit { - function initialize( - address token, - address hats, - uint256[] calldata creatorHats, - address executor, - address deployer - ) external; + function initialize(address token, address hats, uint256[] calldata creatorHats, address executor, address deployer) + external; } interface IEducationHubInit { diff --git a/test/DeployerTest.t.sol b/test/DeployerTest.t.sol index a3bb4e0..91c0a86 100644 --- a/test/DeployerTest.t.sol +++ b/test/DeployerTest.t.sol @@ -250,7 +250,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { /// @dev Helper to build empty bootstrap config function _emptyBootstrap() internal pure returns (OrgDeployer.BootstrapConfig memory) { - ITaskManagerBootstrap.BootstrapProjectConfig[] memory projects = new ITaskManagerBootstrap.BootstrapProjectConfig[](0); + ITaskManagerBootstrap.BootstrapProjectConfig[] memory projects = + new ITaskManagerBootstrap.BootstrapProjectConfig[](0); ITaskManagerBootstrap.BootstrapTaskConfig[] memory tasks = new ITaskManagerBootstrap.BootstrapTaskConfig[](0); return OrgDeployer.BootstrapConfig({projects: projects, tasks: tasks}); } diff --git a/test/TaskManager.t.sol b/test/TaskManager.t.sol index 4830011..34008c0 100644 --- a/test/TaskManager.t.sol +++ b/test/TaskManager.t.sol @@ -4843,392 +4843,399 @@ contract MockToken is Test, IERC20 { } } -/*────────────────── Bootstrap Test Suite ──────────────────*/ -contract TaskManagerBootstrapTest is Test { - /* test actors */ - address creator1 = makeAddr("creator1"); - address pm1 = makeAddr("pm1"); - address member1 = makeAddr("member1"); - address executor = makeAddr("executor"); - address deployer = makeAddr("deployer"); - address outsider = makeAddr("outsider"); - - uint256 constant CREATOR_HAT = 1; - uint256 constant PM_HAT = 2; - uint256 constant MEMBER_HAT = 3; - - TaskManager tm; - TaskManagerLens lens; - MockToken token; - MockHats hats; - MockERC20 bountyToken; - - function setUp() public { - token = new MockToken(); - hats = new MockHats(); - bountyToken = new MockERC20(); - - hats.mintHat(CREATOR_HAT, creator1); - hats.mintHat(PM_HAT, pm1); - hats.mintHat(MEMBER_HAT, member1); - - tm = new TaskManager(); - lens = new TaskManagerLens(); - uint256[] memory creatorHats = new uint256[](1); - creatorHats[0] = CREATOR_HAT; - - // Initialize with deployer address - tm.initialize(address(token), address(hats), creatorHats, executor, deployer); - - // Set up permissions - vm.prank(executor); - tm.setConfig( - TaskManager.ConfigKey.ROLE_PERM, - abi.encode(PM_HAT, TaskPerm.CREATE | TaskPerm.REVIEW | TaskPerm.ASSIGN) - ); - vm.prank(executor); - tm.setConfig(TaskManager.ConfigKey.ROLE_PERM, abi.encode(MEMBER_HAT, TaskPerm.CLAIM)); - } + /*────────────────── Bootstrap Test Suite ──────────────────*/ + contract TaskManagerBootstrapTest is Test { + /* test actors */ + address creator1 = makeAddr("creator1"); + address pm1 = makeAddr("pm1"); + address member1 = makeAddr("member1"); + address executor = makeAddr("executor"); + address deployer = makeAddr("deployer"); + address outsider = makeAddr("outsider"); - /* ─────────────── Helper Functions ─────────────── */ + uint256 constant CREATOR_HAT = 1; + uint256 constant PM_HAT = 2; + uint256 constant MEMBER_HAT = 3; - function _hatArr(uint256 hat) internal pure returns (uint256[] memory arr) { - arr = new uint256[](1); - arr[0] = hat; - } + TaskManager tm; + TaskManagerLens lens; + MockToken token; + MockHats hats; + MockERC20 bountyToken; - function _buildBootstrapProject( - string memory title, - uint256 cap, - uint256[] memory createHats, - uint256[] memory claimHats, - uint256[] memory reviewHats, - uint256[] memory assignHats - ) internal pure returns (TaskManager.BootstrapProjectConfig memory) { - return TaskManager.BootstrapProjectConfig({ - title: bytes(title), - metadataHash: bytes32(0), - cap: cap, - managers: new address[](0), - createHats: createHats, - claimHats: claimHats, - reviewHats: reviewHats, - assignHats: assignHats - }); - } + function setUp() public { + token = new MockToken(); + hats = new MockHats(); + bountyToken = new MockERC20(); - function _buildBootstrapTask( - uint8 projectIndex, - string memory title, - uint256 payout - ) internal pure returns (TaskManager.BootstrapTaskConfig memory) { - return TaskManager.BootstrapTaskConfig({ - projectIndex: projectIndex, - payout: payout, - title: bytes(title), - metadataHash: bytes32(0), - bountyToken: address(0), - bountyPayout: 0, - requiresApplication: false - }); - } + hats.mintHat(CREATOR_HAT, creator1); + hats.mintHat(PM_HAT, pm1); + hats.mintHat(MEMBER_HAT, member1); - /* ─────────────── Test Cases ─────────────── */ - - function test_BootstrapSingleProjectWithTasks() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); - projects[0] = _buildBootstrapProject( - "Getting Started", - 100 ether, - _hatArr(CREATOR_HAT), - _hatArr(MEMBER_HAT), - _hatArr(PM_HAT), - _hatArr(PM_HAT) - ); - - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](2); - tasks[0] = _buildBootstrapTask(0, "Complete your profile", 10 ether); - tasks[1] = _buildBootstrapTask(0, "Introduce yourself", 5 ether); - - vm.prank(deployer); - bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); - - // Verify project created - assertEq(projectIds.length, 1, "Should create 1 project"); - bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[0])); - (uint256 cap, uint256 spent, bool isManager) = abi.decode(result, (uint256, uint256, bool)); - assertEq(cap, 100 ether, "Project cap should be 100 ether"); - assertEq(spent, 15 ether, "Project spent should be 15 ether (both tasks)"); - - // Verify tasks created - result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); - (uint256 payout, TaskManager.Status status,,, bool requiresApp) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); - assertEq(payout, 10 ether, "Task 0 payout should be 10 ether"); - assertEq(uint8(status), uint8(TaskManager.Status.UNCLAIMED), "Task 0 should be UNCLAIMED"); - assertFalse(requiresApp, "Task 0 should not require application"); - - result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(1)); - (payout,,,, ) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); - assertEq(payout, 5 ether, "Task 1 payout should be 5 ether"); - } + tm = new TaskManager(); + lens = new TaskManagerLens(); + uint256[] memory creatorHats = new uint256[](1); + creatorHats[0] = CREATOR_HAT; - function test_BootstrapMultipleProjects() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](2); - projects[0] = _buildBootstrapProject("Project A", 50 ether, _hatArr(CREATOR_HAT), _hatArr(MEMBER_HAT), _hatArr(PM_HAT), _hatArr(PM_HAT)); - projects[1] = _buildBootstrapProject("Project B", 100 ether, _hatArr(CREATOR_HAT), _hatArr(MEMBER_HAT), _hatArr(PM_HAT), _hatArr(PM_HAT)); + // Initialize with deployer address + tm.initialize(address(token), address(hats), creatorHats, executor, deployer); - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](3); - tasks[0] = _buildBootstrapTask(0, "Task for A", 10 ether); - tasks[1] = _buildBootstrapTask(1, "Task 1 for B", 20 ether); - tasks[2] = _buildBootstrapTask(1, "Task 2 for B", 30 ether); + // Set up permissions + vm.prank(executor); + tm.setConfig( + TaskManager.ConfigKey.ROLE_PERM, + abi.encode(PM_HAT, TaskPerm.CREATE | TaskPerm.REVIEW | TaskPerm.ASSIGN) + ); + vm.prank(executor); + tm.setConfig(TaskManager.ConfigKey.ROLE_PERM, abi.encode(MEMBER_HAT, TaskPerm.CLAIM)); + } - vm.prank(deployer); - bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + /* ─────────────── Helper Functions ─────────────── */ - assertEq(projectIds.length, 2, "Should create 2 projects"); + function _hatArr(uint256 hat) internal pure returns (uint256[] memory arr) { + arr = new uint256[](1); + arr[0] = hat; + } - // Verify Project A spent - bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[0])); - (, uint256 spentA,) = abi.decode(result, (uint256, uint256, bool)); - assertEq(spentA, 10 ether, "Project A spent should be 10 ether"); + function _buildBootstrapProject( + string memory title, + uint256 cap, + uint256[] memory createHats, + uint256[] memory claimHats, + uint256[] memory reviewHats, + uint256[] memory assignHats + ) internal pure returns (TaskManager.BootstrapProjectConfig memory) { + return TaskManager.BootstrapProjectConfig({ + title: bytes(title), + metadataHash: bytes32(0), + cap: cap, + managers: new address[](0), + createHats: createHats, + claimHats: claimHats, + reviewHats: reviewHats, + assignHats: assignHats + }); + } - // Verify Project B spent - result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[1])); - (, uint256 spentB,) = abi.decode(result, (uint256, uint256, bool)); - assertEq(spentB, 50 ether, "Project B spent should be 50 ether"); - } + function _buildBootstrapTask(uint8 projectIndex, string memory title, uint256 payout) + internal + pure + returns (TaskManager.BootstrapTaskConfig memory) + { + return TaskManager.BootstrapTaskConfig({ + projectIndex: projectIndex, + payout: payout, + title: bytes(title), + metadataHash: bytes32(0), + bountyToken: address(0), + bountyPayout: 0, + requiresApplication: false + }); + } - function test_BootstrapWithManagers() public { - address[] memory managers = new address[](1); - managers[0] = pm1; - - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); - projects[0] = TaskManager.BootstrapProjectConfig({ - title: bytes("Managed Project"), - metadataHash: bytes32(0), - cap: 100 ether, - managers: managers, - createHats: _hatArr(CREATOR_HAT), - claimHats: _hatArr(MEMBER_HAT), - reviewHats: _hatArr(PM_HAT), - assignHats: _hatArr(PM_HAT) - }); - - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); - - // Expect the ProjectManagerUpdated event to be emitted - vm.expectEmit(true, true, false, true); - emit TaskManager.ProjectManagerUpdated(bytes32(0), pm1, true); - - vm.prank(deployer); - tm.bootstrapProjectsAndTasks(projects, tasks); - } + /* ─────────────── Test Cases ─────────────── */ - function test_BootstrapWithRolePermissions() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); - projects[0] = _buildBootstrapProject( - "Role Test Project", - 100 ether, - _hatArr(CREATOR_HAT), - _hatArr(MEMBER_HAT), - _hatArr(PM_HAT), - _hatArr(PM_HAT) - ); - - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); - tasks[0] = _buildBootstrapTask(0, "Claimable task", 10 ether); - - vm.prank(deployer); - tm.bootstrapProjectsAndTasks(projects, tasks); - - // member1 has MEMBER_HAT which is in claimHats, should be able to claim - vm.prank(member1); - tm.claimTask(0); - - // Verify claim - bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); - (, TaskManager.Status status, address claimer,,) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); - assertEq(claimer, member1, "member1 should be claimer"); - assertEq(uint8(status), uint8(TaskManager.Status.CLAIMED), "Task should be claimed"); - } + function test_BootstrapSingleProjectWithTasks() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Getting Started", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); - function test_BootstrapWithBountyTasks() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); - projects[0] = _buildBootstrapProject( - "Bounty Project", - 100 ether, - _hatArr(CREATOR_HAT), - _hatArr(MEMBER_HAT), - _hatArr(PM_HAT), - _hatArr(PM_HAT) - ); - - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); - tasks[0] = TaskManager.BootstrapTaskConfig({ - projectIndex: 0, - payout: 10 ether, - title: bytes("Bounty task"), - metadataHash: bytes32(0), - bountyToken: address(bountyToken), - bountyPayout: 5 ether, - requiresApplication: false - }); - - vm.prank(deployer); - bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); - - // Verify bounty info stored - bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_FULL_INFO, abi.encode(0)); - ( - uint256 payout, - uint256 bountyPayoutVal, - address bountyTokenAddr, - , - , - , - - ) = abi.decode(result, (uint256, uint256, address, TaskManager.Status, address, bytes32, bool)); - assertEq(payout, 10 ether, "Payout should be 10 ether"); - assertEq(bountyPayoutVal, 5 ether, "Bounty payout should be 5 ether"); - assertEq(bountyTokenAddr, address(bountyToken), "Bounty token should match"); - } + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](2); + tasks[0] = _buildBootstrapTask(0, "Complete your profile", 10 ether); + tasks[1] = _buildBootstrapTask(0, "Introduce yourself", 5 ether); - function test_BootstrapWithRequiresApplication() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); - projects[0] = _buildBootstrapProject( - "Application Project", - 100 ether, - _hatArr(CREATOR_HAT), - _hatArr(MEMBER_HAT), - _hatArr(PM_HAT), - _hatArr(PM_HAT) - ); - - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); - tasks[0] = TaskManager.BootstrapTaskConfig({ - projectIndex: 0, - payout: 10 ether, - title: bytes("Apply first"), - metadataHash: bytes32(0), - bountyToken: address(0), - bountyPayout: 0, - requiresApplication: true - }); - - vm.prank(deployer); - tm.bootstrapProjectsAndTasks(projects, tasks); - - // Direct claim should fail - vm.prank(member1); - vm.expectRevert(TaskManager.RequiresApplication.selector); - tm.claimTask(0); - - // Apply and then get approved - vm.prank(member1); - tm.applyForTask(0, keccak256("my application")); - - vm.prank(pm1); - tm.approveApplication(0, member1); - - // Now claim should work - bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); - (, TaskManager.Status status, address claimer,,) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); - assertEq(claimer, member1, "member1 should be claimer after approval"); - } + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); - function test_BootstrapOnlyDeployer() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); - projects[0] = _buildBootstrapProject( - "Test Project", - 100 ether, - _hatArr(CREATOR_HAT), - _hatArr(MEMBER_HAT), - _hatArr(PM_HAT), - _hatArr(PM_HAT) - ); - - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); - - // creator1 should not be able to bootstrap - vm.prank(creator1); - vm.expectRevert(TaskManager.NotDeployer.selector); - tm.bootstrapProjectsAndTasks(projects, tasks); - - // executor should not be able to bootstrap - vm.prank(executor); - vm.expectRevert(TaskManager.NotDeployer.selector); - tm.bootstrapProjectsAndTasks(projects, tasks); - - // outsider should not be able to bootstrap - vm.prank(outsider); - vm.expectRevert(TaskManager.NotDeployer.selector); - tm.bootstrapProjectsAndTasks(projects, tasks); - } + // Verify project created + assertEq(projectIds.length, 1, "Should create 1 project"); + bytes memory result = + lens.getStorage(address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[0])); + (uint256 cap, uint256 spent, bool isManager) = abi.decode(result, (uint256, uint256, bool)); + assertEq(cap, 100 ether, "Project cap should be 100 ether"); + assertEq(spent, 15 ether, "Project spent should be 15 ether (both tasks)"); - function test_BootstrapInvalidProjectIndex() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); - projects[0] = _buildBootstrapProject( - "Only Project", - 100 ether, - _hatArr(CREATOR_HAT), - _hatArr(MEMBER_HAT), - _hatArr(PM_HAT), - _hatArr(PM_HAT) - ); - - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); - tasks[0] = _buildBootstrapTask(5, "Invalid reference", 10 ether); // projectIndex 5 doesn't exist - - vm.prank(deployer); - vm.expectRevert(TaskManager.InvalidIndex.selector); - tm.bootstrapProjectsAndTasks(projects, tasks); - } + // Verify tasks created + result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); + (uint256 payout, TaskManager.Status status,,, bool requiresApp) = + abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(payout, 10 ether, "Task 0 payout should be 10 ether"); + assertEq(uint8(status), uint8(TaskManager.Status.UNCLAIMED), "Task 0 should be UNCLAIMED"); + assertFalse(requiresApp, "Task 0 should not require application"); - function test_BootstrapEmptyArrays() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](0); - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(1)); + (payout,,,,) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(payout, 5 ether, "Task 1 payout should be 5 ether"); + } - vm.prank(deployer); - bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + function test_BootstrapMultipleProjects() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](2); + projects[0] = _buildBootstrapProject( + "Project A", 50 ether, _hatArr(CREATOR_HAT), _hatArr(MEMBER_HAT), _hatArr(PM_HAT), _hatArr(PM_HAT) + ); + projects[1] = _buildBootstrapProject( + "Project B", 100 ether, _hatArr(CREATOR_HAT), _hatArr(MEMBER_HAT), _hatArr(PM_HAT), _hatArr(PM_HAT) + ); - assertEq(projectIds.length, 0, "Should return empty array for empty bootstrap"); - } + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](3); + tasks[0] = _buildBootstrapTask(0, "Task for A", 10 ether); + tasks[1] = _buildBootstrapTask(1, "Task 1 for B", 20 ether); + tasks[2] = _buildBootstrapTask(1, "Task 2 for B", 30 ether); - function test_BootstrapTaskLifecycleAfterBootstrap() public { - TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); - projects[0] = _buildBootstrapProject( - "Lifecycle Project", - 100 ether, - _hatArr(CREATOR_HAT), - _hatArr(MEMBER_HAT), - _hatArr(PM_HAT), - _hatArr(PM_HAT) - ); + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); - TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); - tasks[0] = _buildBootstrapTask(0, "Complete me", 10 ether); + assertEq(projectIds.length, 2, "Should create 2 projects"); - vm.prank(deployer); - tm.bootstrapProjectsAndTasks(projects, tasks); + // Verify Project A spent + bytes memory result = + lens.getStorage(address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[0])); + (, uint256 spentA,) = abi.decode(result, (uint256, uint256, bool)); + assertEq(spentA, 10 ether, "Project A spent should be 10 ether"); - // Full lifecycle: claim → submit → complete - uint256 balBefore = token.balanceOf(member1); + // Verify Project B spent + result = lens.getStorage( + address(tm), TaskManagerLens.StorageKey.PROJECT_INFO, abi.encode(projectIds[1]) + ); + (, uint256 spentB,) = abi.decode(result, (uint256, uint256, bool)); + assertEq(spentB, 50 ether, "Project B spent should be 50 ether"); + } - vm.prank(member1); - tm.claimTask(0); + function test_BootstrapWithManagers() public { + address[] memory managers = new address[](1); + managers[0] = pm1; - vm.prank(member1); - tm.submitTask(0, keccak256("my work")); + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = TaskManager.BootstrapProjectConfig({ + title: bytes("Managed Project"), + metadataHash: bytes32(0), + cap: 100 ether, + managers: managers, + createHats: _hatArr(CREATOR_HAT), + claimHats: _hatArr(MEMBER_HAT), + reviewHats: _hatArr(PM_HAT), + assignHats: _hatArr(PM_HAT) + }); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + + // Expect the ProjectManagerUpdated event to be emitted + vm.expectEmit(true, true, false, true); + emit TaskManager.ProjectManagerUpdated(bytes32(0), pm1, true); + + vm.prank(deployer); + tm.bootstrapProjectsAndTasks(projects, tasks); + } - vm.prank(pm1); - tm.completeTask(0); + function test_BootstrapWithRolePermissions() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Role Test Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); - // Verify minting - assertEq(token.balanceOf(member1), balBefore + 10 ether, "Should mint participation tokens"); + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = _buildBootstrapTask(0, "Claimable task", 10 ether); - // Verify task completed - bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); - (, TaskManager.Status status,,,) = abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); - assertEq(uint8(status), uint8(TaskManager.Status.COMPLETED), "Task should be completed"); - } -} + vm.prank(deployer); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // member1 has MEMBER_HAT which is in claimHats, should be able to claim + vm.prank(member1); + tm.claimTask(0); + + // Verify claim + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); + (, TaskManager.Status status, address claimer,,) = + abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(claimer, member1, "member1 should be claimer"); + assertEq(uint8(status), uint8(TaskManager.Status.CLAIMED), "Task should be claimed"); + } + + function test_BootstrapWithBountyTasks() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Bounty Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = TaskManager.BootstrapTaskConfig({ + projectIndex: 0, + payout: 10 ether, + title: bytes("Bounty task"), + metadataHash: bytes32(0), + bountyToken: address(bountyToken), + bountyPayout: 5 ether, + requiresApplication: false + }); + + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + + // Verify bounty info stored + bytes memory result = lens.getStorage( + address(tm), TaskManagerLens.StorageKey.TASK_FULL_INFO, abi.encode(0) + ); + (uint256 payout, uint256 bountyPayoutVal, address bountyTokenAddr,,,,) = + abi.decode(result, (uint256, uint256, address, TaskManager.Status, address, bytes32, bool)); + assertEq(payout, 10 ether, "Payout should be 10 ether"); + assertEq(bountyPayoutVal, 5 ether, "Bounty payout should be 5 ether"); + assertEq(bountyTokenAddr, address(bountyToken), "Bounty token should match"); + } + + function test_BootstrapWithRequiresApplication() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Application Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = TaskManager.BootstrapTaskConfig({ + projectIndex: 0, + payout: 10 ether, + title: bytes("Apply first"), + metadataHash: bytes32(0), + bountyToken: address(0), + bountyPayout: 0, + requiresApplication: true + }); + + vm.prank(deployer); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // Direct claim should fail + vm.prank(member1); + vm.expectRevert(TaskManager.RequiresApplication.selector); + tm.claimTask(0); + + // Apply and then get approved + vm.prank(member1); + tm.applyForTask(0, keccak256("my application")); + + vm.prank(pm1); + tm.approveApplication(0, member1); + + // Now claim should work + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); + (, TaskManager.Status status, address claimer,,) = + abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(claimer, member1, "member1 should be claimer after approval"); + } + + function test_BootstrapOnlyDeployer() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Test Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + + // creator1 should not be able to bootstrap + vm.prank(creator1); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // executor should not be able to bootstrap + vm.prank(executor); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // outsider should not be able to bootstrap + vm.prank(outsider); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + } + + function test_BootstrapInvalidProjectIndex() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Only Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = _buildBootstrapTask(5, "Invalid reference", 10 ether); // projectIndex 5 doesn't exist + + vm.prank(deployer); + vm.expectRevert(TaskManager.InvalidIndex.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + } + + function test_BootstrapEmptyArrays() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](0); + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + + assertEq(projectIds.length, 0, "Should return empty array for empty bootstrap"); + } + + function test_BootstrapTaskLifecycleAfterBootstrap() public { + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Lifecycle Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = _buildBootstrapTask(0, "Complete me", 10 ether); + + vm.prank(deployer); + tm.bootstrapProjectsAndTasks(projects, tasks); + + // Full lifecycle: claim → submit → complete + uint256 balBefore = token.balanceOf(member1); + + vm.prank(member1); + tm.claimTask(0); + + vm.prank(member1); + tm.submitTask(0, keccak256("my work")); + + vm.prank(pm1); + tm.completeTask(0); + + // Verify minting + assertEq(token.balanceOf(member1), balBefore + 10 ether, "Should mint participation tokens"); + + // Verify task completed + bytes memory result = lens.getStorage(address(tm), TaskManagerLens.StorageKey.TASK_INFO, abi.encode(0)); + (, TaskManager.Status status,,,) = + abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); + assertEq(uint8(status), uint8(TaskManager.Status.COMPLETED), "Task should be completed"); + } + } From 994aae08584728a4d82385f0fbb7f7af996c6835 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Fri, 23 Jan 2026 16:29:57 -0500 Subject: [PATCH 5/9] feat: add clearDeployer to prevent post-deployment bootstrap calls Defense-in-depth security improvement for the bootstrap system: - Add clearDeployer() function to TaskManager that can only be called by the deployer to permanently revoke bootstrap privileges - OrgDeployer now calls clearDeployer() after bootstrap phase completes - This prevents any future bootstrapProjectsAndTasks calls even if OrgDeployer were compromised Tests added: - test_ClearDeployerSuccess - test_ClearDeployerOnlyDeployer - test_ClearDeployerCannotBeCalledTwice - test_BootstrapThenClear - testFullOrgDeploymentWithBootstrapAndClearDeployer Co-Authored-By: Claude Opus 4.5 --- src/OrgDeployer.sol | 5 ++ src/TaskManager.sol | 11 ++++ test/DeployerTest.t.sol | 130 ++++++++++++++++++++++++++++++++++++++++ test/TaskManager.t.sol | 91 ++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+) diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index 2f6a985..1ad2a52 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -68,6 +68,8 @@ interface ITaskManagerBootstrap { function bootstrapProjectsAndTasks(BootstrapProjectConfig[] calldata projects, BootstrapTaskConfig[] calldata tasks) external returns (bytes32[] memory projectIds); + + function clearDeployer() external; } /** @@ -411,6 +413,9 @@ contract OrgDeployer is Initializable { .bootstrapProjectsAndTasks(resolvedProjects, params.bootstrap.tasks); } + /* 8.6. Clear deployer address to prevent future bootstrap calls (defense-in-depth) */ + ITaskManagerBootstrap(result.taskManager).clearDeployer(); + /* 9. Authorize QuickJoin to mint hats */ IExecutorAdmin(result.executor).setHatMinterAuthorization(result.quickJoin, true); diff --git a/src/TaskManager.sol b/src/TaskManager.sol index e675ef2..f79fd86 100644 --- a/src/TaskManager.sol +++ b/src/TaskManager.sol @@ -357,6 +357,17 @@ contract TaskManager is Initializable, ContextUpgradeable { } } + /** + * @notice Clear the deployer address after bootstrap phase is complete + * @dev Only callable by deployer. Prevents future bootstrap calls for defense-in-depth. + * Should be called by OrgDeployer at the end of org deployment. + */ + function clearDeployer() external { + Layout storage l = _layout(); + if (_msgSender() != l.deployer) revert NotDeployer(); + l.deployer = address(0); + } + /*──────── Task Logic ───────*/ function createTask( uint256 payout, diff --git a/test/DeployerTest.t.sol b/test/DeployerTest.t.sol index 91c0a86..20fd8f1 100644 --- a/test/DeployerTest.t.sol +++ b/test/DeployerTest.t.sol @@ -256,6 +256,56 @@ contract DeployerTest is Test, IEligibilityModuleEvents { return OrgDeployer.BootstrapConfig({projects: projects, tasks: tasks}); } + /// @dev Helper to build bootstrap config with one project and two tasks + function _buildBootstrapWithTasks() internal pure returns (OrgDeployer.BootstrapConfig memory) { + ITaskManagerBootstrap.BootstrapProjectConfig[] memory projects = + new ITaskManagerBootstrap.BootstrapProjectConfig[](1); + + uint256[] memory createRoles = new uint256[](1); + createRoles[0] = 1; // EXECUTIVE role index + uint256[] memory claimRoles = new uint256[](2); + claimRoles[0] = 0; // DEFAULT role index + claimRoles[1] = 1; // EXECUTIVE role index + uint256[] memory reviewRoles = new uint256[](1); + reviewRoles[0] = 1; // EXECUTIVE role index + uint256[] memory assignRoles = new uint256[](1); + assignRoles[0] = 1; // EXECUTIVE role index + address[] memory managers = new address[](0); + + projects[0] = ITaskManagerBootstrap.BootstrapProjectConfig({ + title: bytes("Getting Started"), + metadataHash: bytes32(0), + cap: 1000 ether, + managers: managers, + createHats: createRoles, + claimHats: claimRoles, + reviewHats: reviewRoles, + assignHats: assignRoles + }); + + ITaskManagerBootstrap.BootstrapTaskConfig[] memory tasks = new ITaskManagerBootstrap.BootstrapTaskConfig[](2); + tasks[0] = ITaskManagerBootstrap.BootstrapTaskConfig({ + projectIndex: 0, + payout: 10 ether, + title: bytes("Complete your profile"), + metadataHash: bytes32(0), + bountyToken: address(0), + bountyPayout: 0, + requiresApplication: false + }); + tasks[1] = ITaskManagerBootstrap.BootstrapTaskConfig({ + projectIndex: 0, + payout: 5 ether, + title: bytes("Introduce yourself"), + metadataHash: bytes32(0), + bountyToken: address(0), + bountyPayout: 0, + requiresApplication: false + }); + + return OrgDeployer.BootstrapConfig({projects: projects, tasks: tasks}); + } + /// @dev Helper to build legacy-style voting classes function _buildLegacyClasses(uint8 ddSplit, uint8 ptSplit, bool quadratic, uint256 minBal) internal @@ -834,6 +884,86 @@ contract DeployerTest is Test, IEligibilityModuleEvents { assertEq(paymentManager.owner(), exec, "PaymentManager owner should be executor"); } + function testFullOrgDeploymentWithBootstrapAndClearDeployer() public { + /*–––– deploy a full org with bootstrap config ––––*/ + vm.startPrank(orgOwner); + + string[] memory names = new string[](2); + names[0] = "DEFAULT"; + names[1] = "EXECUTIVE"; + string[] memory images = new string[](2); + images[0] = "ipfs://default-role-image"; + images[1] = "ipfs://executive-role-image"; + bool[] memory voting = new bool[](2); + voting[0] = true; + voting[1] = true; + + IHybridVotingInit.ClassConfig[] memory classes = _buildLegacyClasses(50, 50, false, 4 ether); + OrgDeployer.RoleAssignments memory roleAssignments = _buildDefaultRoleAssignments(); + address[] memory ddTargets = new address[](0); + + OrgDeployer.DeploymentParams memory params = OrgDeployer.DeploymentParams({ + orgId: ORG_ID, + orgName: "Bootstrap DAO", + metadataHash: bytes32(0), + registryAddr: accountRegProxy, + deployerAddress: orgOwner, + deployerUsername: "", + autoUpgrade: true, + hybridQuorumPct: 50, + ddQuorumPct: 50, + hybridClasses: classes, + ddInitialTargets: ddTargets, + roles: _buildSimpleRoleConfigs(names, images, voting), + roleAssignments: roleAssignments, + passkeyEnabled: false, + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _buildBootstrapWithTasks() + }); + + OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); + + vm.stopPrank(); + + // Verify TaskManager was deployed + assertTrue(result.taskManager != address(0), "TaskManager should be deployed"); + + // After deployment, deployer address should be cleared + // Attempting to call bootstrap again should fail with NotDeployer + ITaskManagerBootstrap.BootstrapProjectConfig[] memory moreProjects = + new ITaskManagerBootstrap.BootstrapProjectConfig[](1); + + uint256[] memory createRoles = new uint256[](1); + createRoles[0] = 1; + uint256[] memory claimRoles = new uint256[](1); + claimRoles[0] = 0; + address[] memory managers = new address[](0); + + moreProjects[0] = ITaskManagerBootstrap.BootstrapProjectConfig({ + title: bytes("Second Project"), + metadataHash: bytes32(0), + cap: 100 ether, + managers: managers, + createHats: createRoles, + claimHats: claimRoles, + reviewHats: createRoles, + assignHats: createRoles + }); + + ITaskManagerBootstrap.BootstrapTaskConfig[] memory moreTasks = + new ITaskManagerBootstrap.BootstrapTaskConfig[](0); + + // OrgDeployer should no longer be able to bootstrap (deployer was cleared) + vm.prank(address(deployer)); + vm.expectRevert(TaskManager.NotDeployer.selector); + ITaskManagerBootstrap(result.taskManager).bootstrapProjectsAndTasks(moreProjects, moreTasks); + + // clearDeployer should also fail since deployer is already cleared + vm.prank(address(deployer)); + vm.expectRevert(TaskManager.NotDeployer.selector); + ITaskManagerBootstrap(result.taskManager).clearDeployer(); + } + function testDeployFullOrgMismatchExecutorReverts() public { _deployFullOrg(); address other = address(99); diff --git a/test/TaskManager.t.sol b/test/TaskManager.t.sol index 34008c0..a7c903a 100644 --- a/test/TaskManager.t.sol +++ b/test/TaskManager.t.sol @@ -5238,4 +5238,95 @@ contract MockToken is Test, IERC20 { abi.decode(result, (uint256, TaskManager.Status, address, bytes32, bool)); assertEq(uint8(status), uint8(TaskManager.Status.COMPLETED), "Task should be completed"); } + + /*───────────────────────────────────────────────────────────────────────────── + clearDeployer Tests + ─────────────────────────────────────────────────────────────────────────────*/ + + function test_ClearDeployerSuccess() public { + // deployer can clear themselves + vm.prank(deployer); + tm.clearDeployer(); + + // After clearing, deployer should no longer be able to bootstrap + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Should Fail", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + + vm.prank(deployer); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + } + + function test_ClearDeployerOnlyDeployer() public { + // Non-deployer cannot clear deployer + vm.prank(creator1); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.clearDeployer(); + + vm.prank(executor); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.clearDeployer(); + + vm.prank(outsider); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.clearDeployer(); + } + + function test_ClearDeployerCannotBeCalledTwice() public { + // First clear succeeds + vm.prank(deployer); + tm.clearDeployer(); + + // Second clear fails (deployer is now address(0)) + vm.prank(deployer); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.clearDeployer(); + } + + function test_BootstrapThenClear() public { + // Bootstrap first + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Initial Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = _buildBootstrapTask(0, "Initial Task", 10 ether); + + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + assertEq(projectIds.length, 1, "Should create 1 project"); + + // Clear deployer + vm.prank(deployer); + tm.clearDeployer(); + + // Cannot bootstrap again + TaskManager.BootstrapProjectConfig[] memory moreProjects = new TaskManager.BootstrapProjectConfig[](1); + moreProjects[0] = _buildBootstrapProject( + "Second Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + TaskManager.BootstrapTaskConfig[] memory moreTasks = new TaskManager.BootstrapTaskConfig[](0); + + vm.prank(deployer); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(moreProjects, moreTasks); + } } From 8877affae34963ff5072880b1d9d282e83bd1a94 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Fri, 23 Jan 2026 17:28:40 -0500 Subject: [PATCH 6/9] refactor: remove mintToExecutor and unconditional executor eligibility The executor receives admin authority via the topHat transfer, not by wearing individual role hats. This change removes unnecessary overhead: - Remove mintToExecutor field from RoleDistributionConfig struct - Remove executor from eligibility setup (was added to every role) - Remove executor from minting loop - Update deployment scripts and JSON configs - Update tests to reflect new struct shape The executor can still mint hats to users via its topHat admin authority. All 562 tests pass. Co-Authored-By: Claude Opus 4.5 --- script/DeployOrg.s.sol | 4 ---- script/RunOrgActions.s.sol | 4 ---- script/RunOrgActionsAdvanced.s.sol | 4 ---- script/org-config-advanced-demo.json | 4 ---- src/HatsTreeSetup.sol | 15 +-------------- src/libs/RoleConfigStructs.sol | 1 - test/DeployerTest.t.sol | 12 +++++------- 7 files changed, 6 insertions(+), 38 deletions(-) diff --git a/script/DeployOrg.s.sol b/script/DeployOrg.s.sol index 8cf4726..2ccbdbf 100644 --- a/script/DeployOrg.s.sol +++ b/script/DeployOrg.s.sol @@ -66,7 +66,6 @@ contract DeployOrg is Script { struct RoleDistributionConfigJson { bool mintToDeployer; - bool mintToExecutor; address[] additionalWearers; } @@ -274,8 +273,6 @@ contract DeployOrg is Script { bool mintToDeployer ) { config.roles[i].distribution.mintToDeployer = mintToDeployer; - config.roles[i].distribution.mintToExecutor = - vm.parseJsonBool(configJson, string.concat(basePath, ".distribution.mintToExecutor")); bytes memory additionalWearersData = vm.parseJson(configJson, string.concat(basePath, ".distribution.additionalWearers")); config.roles[i].distribution.additionalWearers = abi.decode(additionalWearersData, (address[])); @@ -506,7 +503,6 @@ contract DeployOrg is Script { hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: role.hierarchy.adminRoleIndex}), distribution: RoleConfigStructs.RoleDistributionConfig({ mintToDeployer: role.distribution.mintToDeployer, - mintToExecutor: role.distribution.mintToExecutor, additionalWearers: role.distribution.additionalWearers }), hatConfig: RoleConfigStructs.HatConfig({ diff --git a/script/RunOrgActions.s.sol b/script/RunOrgActions.s.sol index 379463d..fec5833 100644 --- a/script/RunOrgActions.s.sol +++ b/script/RunOrgActions.s.sol @@ -89,7 +89,6 @@ contract RunOrgActions is Script { struct RoleDistributionConfigJson { bool mintToDeployer; - bool mintToExecutor; address[] additionalWearers; } @@ -684,8 +683,6 @@ contract RunOrgActions is Script { bool mintToDeployer ) { config.roles[i].distribution.mintToDeployer = mintToDeployer; - config.roles[i].distribution.mintToExecutor = - vm.parseJsonBool(configJson, string.concat(basePath, ".distribution.mintToExecutor")); bytes memory additionalWearersData = vm.parseJson(configJson, string.concat(basePath, ".distribution.additionalWearers")); config.roles[i].distribution.additionalWearers = abi.decode(additionalWearersData, (address[])); @@ -911,7 +908,6 @@ contract RunOrgActions is Script { hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: role.hierarchy.adminRoleIndex}), distribution: RoleConfigStructs.RoleDistributionConfig({ mintToDeployer: role.distribution.mintToDeployer, - mintToExecutor: role.distribution.mintToExecutor, additionalWearers: role.distribution.additionalWearers }), hatConfig: RoleConfigStructs.HatConfig({ diff --git a/script/RunOrgActionsAdvanced.s.sol b/script/RunOrgActionsAdvanced.s.sol index c618806..ca9dfdf 100644 --- a/script/RunOrgActionsAdvanced.s.sol +++ b/script/RunOrgActionsAdvanced.s.sol @@ -93,7 +93,6 @@ contract RunOrgActionsAdvanced is Script { struct RoleDistributionConfigJson { bool mintToDeployer; - bool mintToExecutor; address[] additionalWearers; } @@ -801,8 +800,6 @@ contract RunOrgActionsAdvanced is Script { bool mintToDeployer ) { config.roles[i].distribution.mintToDeployer = mintToDeployer; - config.roles[i].distribution.mintToExecutor = - vm.parseJsonBool(configJson, string.concat(basePath, ".distribution.mintToExecutor")); bytes memory additionalWearersData = vm.parseJson(configJson, string.concat(basePath, ".distribution.additionalWearers")); config.roles[i].distribution.additionalWearers = abi.decode(additionalWearersData, (address[])); @@ -1028,7 +1025,6 @@ contract RunOrgActionsAdvanced is Script { hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: role.hierarchy.adminRoleIndex}), distribution: RoleConfigStructs.RoleDistributionConfig({ mintToDeployer: role.distribution.mintToDeployer, - mintToExecutor: role.distribution.mintToExecutor, additionalWearers: role.distribution.additionalWearers }), hatConfig: RoleConfigStructs.HatConfig({ diff --git a/script/org-config-advanced-demo.json b/script/org-config-advanced-demo.json index c2980ee..39d1026 100644 --- a/script/org-config-advanced-demo.json +++ b/script/org-config-advanced-demo.json @@ -26,7 +26,6 @@ }, "distribution": { "mintToDeployer": false, - "mintToExecutor": false, "additionalWearers": [] }, "hatConfig": { @@ -53,7 +52,6 @@ }, "distribution": { "mintToDeployer": false, - "mintToExecutor": false, "additionalWearers": [] }, "hatConfig": { @@ -80,7 +78,6 @@ }, "distribution": { "mintToDeployer": false, - "mintToExecutor": false, "additionalWearers": [] }, "hatConfig": { @@ -107,7 +104,6 @@ }, "distribution": { "mintToDeployer": true, - "mintToExecutor": false, "additionalWearers": [] }, "hatConfig": { diff --git a/src/HatsTreeSetup.sol b/src/HatsTreeSetup.sol index 48ed03d..85c3c28 100644 --- a/src/HatsTreeSetup.sol +++ b/src/HatsTreeSetup.sol @@ -161,10 +161,9 @@ contract HatsTreeSetup { ); // Step 5: Collect all eligibility and toggle operations for batch execution - // Count total eligibility entries needed: executor (always) + deployer (only if minting) + additional wearers + // Count total eligibility entries needed: deployer (only if minting) + additional wearers uint256 eligibilityCount = 0; for (uint256 i = 0; i < len; i++) { - eligibilityCount += 1; // executor always eligible // Deployer only eligible if they're receiving the hat (matches minting conditions) if (params.roles[i].canVote && params.roles[i].distribution.mintToDeployer) { eligibilityCount += 1; @@ -190,11 +189,6 @@ contract HatsTreeSetup { uint256 hatId = result.roleHatIds[i]; RoleConfigStructs.RoleConfig memory role = params.roles[i]; - // Executor always eligible (needed for QuickJoin and governance operations) - eligWearers[eligIndex] = params.executor; - eligHatIds[eligIndex] = hatId; - eligIndex++; - // Deployer only eligible if they're receiving the hat (matches minting conditions) if (role.canVote && role.distribution.mintToDeployer) { eligWearers[eligIndex] = params.deployerAddress; @@ -233,7 +227,6 @@ contract HatsTreeSetup { if (!role.canVote) continue; if (role.distribution.mintToDeployer) mintCount++; - if (role.distribution.mintToExecutor) mintCount++; mintCount += role.distribution.additionalWearers.length; } @@ -262,12 +255,6 @@ contract HatsTreeSetup { mintIndex++; } - if (role.distribution.mintToExecutor) { - hatIdsToMint[mintIndex] = hatId; - wearersToMint[mintIndex] = params.executor; - mintIndex++; - } - for (uint256 j = 0; j < role.distribution.additionalWearers.length; j++) { hatIdsToMint[mintIndex] = hatId; wearersToMint[mintIndex] = role.distribution.additionalWearers[j]; diff --git a/src/libs/RoleConfigStructs.sol b/src/libs/RoleConfigStructs.sol index 9311de4..a3d0f98 100644 --- a/src/libs/RoleConfigStructs.sol +++ b/src/libs/RoleConfigStructs.sol @@ -34,7 +34,6 @@ library RoleConfigStructs { /// @dev Controls who gets the role minted to them initially struct RoleDistributionConfig { bool mintToDeployer; // Mint to deployer address - bool mintToExecutor; // Mint to executor contract address[] additionalWearers; // Additional addresses to mint to } diff --git a/test/DeployerTest.t.sol b/test/DeployerTest.t.sol index 20fd8f1..d7aefae 100644 --- a/test/DeployerTest.t.sol +++ b/test/DeployerTest.t.sol @@ -215,9 +215,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { adminRoleIndex: isTopRole ? type(uint256).max : i + 1 }), distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: isTopRole && canVote[i], - mintToExecutor: !isTopRole && canVote[i], - additionalWearers: new address[](0) + mintToDeployer: isTopRole && canVote[i], additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({ maxSupply: type(uint32).max, // Default: unlimited @@ -1921,7 +1919,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { defaults: RoleConfigStructs.RoleEligibilityDefaults({eligible: true, standing: true}), hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: type(uint256).max}), distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: false, mintToExecutor: true, additionalWearers: new address[](0) + mintToDeployer: false, additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({maxSupply: type(uint32).max, mutableHat: true}) }); @@ -1936,7 +1934,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { defaults: RoleConfigStructs.RoleEligibilityDefaults({eligible: true, standing: true}), hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: type(uint256).max}), distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: true, mintToExecutor: false, additionalWearers: new address[](0) + mintToDeployer: true, additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({maxSupply: type(uint32).max, mutableHat: true}) }); @@ -1961,7 +1959,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { defaults: RoleConfigStructs.RoleEligibilityDefaults({eligible: true, standing: true}), hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: type(uint256).max}), distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: true, mintToExecutor: false, additionalWearers: new address[](0) + mintToDeployer: true, additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({maxSupply: type(uint32).max, mutableHat: true}) }); @@ -1983,7 +1981,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { defaults: RoleConfigStructs.RoleEligibilityDefaults({eligible: true, standing: true}), hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: 0}), // Self-reference distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: true, mintToExecutor: false, additionalWearers: new address[](0) + mintToDeployer: true, additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({maxSupply: type(uint32).max, mutableHat: true}) }); From d5751cb16988460b390e7fb3d86fc57a01d14851 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Fri, 23 Jan 2026 21:06:46 -0500 Subject: [PATCH 7/9] feat: add InitialWearersAssigned event for subgraph User creation During deployment, WearerEligibilityUpdated events are emitted before OrgDeployed, causing a timing issue in the subgraph where User entities cannot be created (EligibilityModuleContract doesn't exist yet). This PR adds a new InitialWearersAssigned event emitted after OrgDeployed that includes all addresses who received hats during deployment. The subgraph can then process this event to create User and RoleWearer entities with proper organization context. Changes: - Add InitialWearersAssigned event definition - Add _collectInitialWearers helper function - Emit event after OrgDeployed with wearer-to-hat mapping Co-Authored-By: Claude Opus 4.5 --- src/OrgDeployer.sol | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index 73656aa..b9c840f 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -79,6 +79,14 @@ contract OrgDeployer is Initializable { bytes32 indexed orgId, uint256[] hatIds, string[] names, string[] images, bytes32[] metadataCIDs, bool[] canVote ); + /// @notice Emitted after OrgDeployed to provide initial wearer assignments for subgraph indexing + event InitialWearersAssigned( + bytes32 indexed orgId, + address indexed eligibilityModule, + address[] wearers, + uint256[] hatIds + ); + /*───────────── ERC-7201 Storage ───────────*/ /// @custom:storage-location erc7201:poa.orgdeployer.storage struct Layout { @@ -426,6 +434,25 @@ contract OrgDeployer is Initializable { gov.roleHatIds ); + /* 12b. Emit initial wearer assignments for subgraph User creation */ + { + (address[] memory wearers, uint256[] memory hatIds) = _collectInitialWearers( + params.roles, + gov.roleHatIds, + params.deployerAddress, + result.executor + ); + + if (wearers.length > 0) { + emit InitialWearersAssigned( + params.orgId, + gov.eligibilityModule, + wearers, + hatIds + ); + } + } + /* 13. Emit role metadata for subgraph indexing */ { uint256 roleCount = params.roles.length; @@ -454,6 +481,52 @@ contract OrgDeployer is Initializable { return exists; } + /** + * @notice Collects all initial wearers from role configurations + * @dev Used to emit InitialWearersAssigned event for subgraph indexing + */ + function _collectInitialWearers( + RoleConfigStructs.RoleConfig[] calldata roles, + uint256[] memory roleHatIds, + address deployerAddress, + address executor + ) internal pure returns (address[] memory wearers, uint256[] memory hatIds) { + // First pass: count total wearers + uint256 totalCount = 0; + for (uint256 i = 0; i < roles.length; i++) { + if (!roles[i].canVote) continue; + if (roles[i].distribution.mintToDeployer) totalCount++; + if (roles[i].distribution.mintToExecutor) totalCount++; + totalCount += roles[i].distribution.additionalWearers.length; + } + + // Second pass: populate arrays + wearers = new address[](totalCount); + hatIds = new uint256[](totalCount); + uint256 idx = 0; + + for (uint256 i = 0; i < roles.length; i++) { + if (!roles[i].canVote) continue; + uint256 hatId = roleHatIds[i]; + + if (roles[i].distribution.mintToDeployer) { + wearers[idx] = deployerAddress; + hatIds[idx] = hatId; + idx++; + } + if (roles[i].distribution.mintToExecutor) { + wearers[idx] = executor; + hatIds[idx] = hatId; + idx++; + } + for (uint256 j = 0; j < roles[i].distribution.additionalWearers.length; j++) { + wearers[idx] = roles[i].distribution.additionalWearers[j]; + hatIds[idx] = hatId; + idx++; + } + } + } + /** * @notice Internal helper to deploy governance infrastructure * @dev Extracted to reduce stack depth in main deployment function From fd3e417b60b5449e444a3347717277f2b07491c8 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Wed, 28 Jan 2026 15:52:27 -0500 Subject: [PATCH 8/9] chore: fmt --- src/OrgDeployer.sol | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index b9c840f..10b09d8 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -81,10 +81,7 @@ contract OrgDeployer is Initializable { /// @notice Emitted after OrgDeployed to provide initial wearer assignments for subgraph indexing event InitialWearersAssigned( - bytes32 indexed orgId, - address indexed eligibilityModule, - address[] wearers, - uint256[] hatIds + bytes32 indexed orgId, address indexed eligibilityModule, address[] wearers, uint256[] hatIds ); /*───────────── ERC-7201 Storage ───────────*/ @@ -436,20 +433,11 @@ contract OrgDeployer is Initializable { /* 12b. Emit initial wearer assignments for subgraph User creation */ { - (address[] memory wearers, uint256[] memory hatIds) = _collectInitialWearers( - params.roles, - gov.roleHatIds, - params.deployerAddress, - result.executor - ); + (address[] memory wearers, uint256[] memory hatIds) = + _collectInitialWearers(params.roles, gov.roleHatIds, params.deployerAddress, result.executor); if (wearers.length > 0) { - emit InitialWearersAssigned( - params.orgId, - gov.eligibilityModule, - wearers, - hatIds - ); + emit InitialWearersAssigned(params.orgId, gov.eligibilityModule, wearers, hatIds); } } From b85a9628818acb4ddbc262caee463753d947c2db Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Wed, 28 Jan 2026 15:55:45 -0500 Subject: [PATCH 9/9] fix: resolve mintToExecutor reference after PR 80 merge The _collectInitialWearers function referenced the removed mintToExecutor field from RoleDistributionConfig. This fix aligns the function with PR 80's changes that removed mintToExecutor from the struct. Co-Authored-By: Claude Opus 4.5 --- src/OrgDeployer.sol | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index 1b0c77d..ca6225d 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -481,7 +481,7 @@ contract OrgDeployer is Initializable { /* 12b. Emit initial wearer assignments for subgraph User creation */ { (address[] memory wearers, uint256[] memory hatIds) = - _collectInitialWearers(params.roles, gov.roleHatIds, params.deployerAddress, result.executor); + _collectInitialWearers(params.roles, gov.roleHatIds, params.deployerAddress); if (wearers.length > 0) { emit InitialWearersAssigned(params.orgId, gov.eligibilityModule, wearers, hatIds); @@ -523,15 +523,13 @@ contract OrgDeployer is Initializable { function _collectInitialWearers( RoleConfigStructs.RoleConfig[] calldata roles, uint256[] memory roleHatIds, - address deployerAddress, - address executor + address deployerAddress ) internal pure returns (address[] memory wearers, uint256[] memory hatIds) { // First pass: count total wearers uint256 totalCount = 0; for (uint256 i = 0; i < roles.length; i++) { if (!roles[i].canVote) continue; if (roles[i].distribution.mintToDeployer) totalCount++; - if (roles[i].distribution.mintToExecutor) totalCount++; totalCount += roles[i].distribution.additionalWearers.length; } @@ -549,11 +547,6 @@ contract OrgDeployer is Initializable { hatIds[idx] = hatId; idx++; } - if (roles[i].distribution.mintToExecutor) { - wearers[idx] = executor; - hatIds[idx] = hatId; - idx++; - } for (uint256 j = 0; j < roles[i].distribution.additionalWearers.length; j++) { wearers[idx] = roles[i].distribution.additionalWearers[j]; hatIds[idx] = hatId;