From d617fb375e4136e5aeb8750ddab386ac40e23cf1 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Tue, 9 Dec 2025 16:38:11 -0500 Subject: [PATCH 1/2] 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/2] 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"); + } +}