diff --git a/script/DeployOrg.s.sol b/script/DeployOrg.s.sol index 01a2fae..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"; @@ -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 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; + } + /*═══════════════════════════ OUTPUT ═══════════════════════════*/ function _outputDeployment(OrgConfigJson memory config, OrgDeployer.DeploymentResult memory result) internal view { 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/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); } 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"); + } +}