diff --git a/CLAUDE.md b/CLAUDE.md index 38b8cb1..2c84169 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,7 @@ ## Rules - Do NOT run `foundryup` - Foundry is pre-installed - ALWAYS run `forge fmt` before commits and PRs +- Do NOT edit `upgrades/` folder - auto-generated by CI ## Project Solidity smart contracts for decentralized organizations (Foundry/Solidity ^0.8.20). diff --git a/script/DeployOrg.s.sol b/script/DeployOrg.s.sol index c3a773e..2ccbdbf 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 { @@ -65,7 +66,6 @@ contract DeployOrg is Script { struct RoleDistributionConfigJson { bool mintToDeployer; - bool mintToExecutor; address[] additionalWearers; } @@ -106,6 +106,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 { @@ -246,8 +273,6 @@ contract DeployOrg is Script { bool mintToDeployer ) { config.roles[i].distribution.mintToDeployer = mintToDeployer; - config.roles[i].distribution.mintToExecutor = - vm.parseJsonBool(configJson, string.concat(basePath, ".distribution.mintToExecutor")); bytes memory additionalWearersData = vm.parseJson(configJson, string.concat(basePath, ".distribution.additionalWearers")); config.roles[i].distribution.additionalWearers = abi.decode(additionalWearersData, (address[])); @@ -325,9 +350,104 @@ 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 ═══════════════════════════*/ /** @@ -383,7 +503,6 @@ contract DeployOrg is Script { hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: role.hierarchy.adminRoleIndex}), distribution: RoleConfigStructs.RoleDistributionConfig({ mintToDeployer: role.distribution.mintToDeployer, - mintToExecutor: role.distribution.mintToExecutor, additionalWearers: role.distribution.additionalWearers }), hatConfig: RoleConfigStructs.HatConfig({ @@ -433,9 +552,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 3247438..fec5833 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 { @@ -88,7 +89,6 @@ contract RunOrgActions is Script { struct RoleDistributionConfigJson { bool mintToDeployer; - bool mintToExecutor; address[] additionalWearers; } @@ -129,6 +129,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; @@ -656,8 +683,6 @@ contract RunOrgActions is Script { bool mintToDeployer ) { config.roles[i].distribution.mintToDeployer = mintToDeployer; - config.roles[i].distribution.mintToExecutor = - vm.parseJsonBool(configJson, string.concat(basePath, ".distribution.mintToExecutor")); bytes memory additionalWearersData = vm.parseJson(configJson, string.concat(basePath, ".distribution.additionalWearers")); config.roles[i].distribution.additionalWearers = abi.decode(additionalWearersData, (address[])); @@ -736,9 +761,104 @@ 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) { @@ -788,7 +908,6 @@ contract RunOrgActions is Script { hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: role.hierarchy.adminRoleIndex}), distribution: RoleConfigStructs.RoleDistributionConfig({ mintToDeployer: role.distribution.mintToDeployer, - mintToExecutor: role.distribution.mintToExecutor, additionalWearers: role.distribution.additionalWearers }), hatConfig: RoleConfigStructs.HatConfig({ @@ -838,6 +957,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 fb7b7e3..ca9dfdf 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 { @@ -92,7 +93,6 @@ contract RunOrgActionsAdvanced is Script { struct RoleDistributionConfigJson { bool mintToDeployer; - bool mintToExecutor; address[] additionalWearers; } @@ -133,6 +133,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; @@ -773,8 +800,6 @@ contract RunOrgActionsAdvanced is Script { bool mintToDeployer ) { config.roles[i].distribution.mintToDeployer = mintToDeployer; - config.roles[i].distribution.mintToExecutor = - vm.parseJsonBool(configJson, string.concat(basePath, ".distribution.mintToExecutor")); bytes memory additionalWearersData = vm.parseJson(configJson, string.concat(basePath, ".distribution.additionalWearers")); config.roles[i].distribution.additionalWearers = abi.decode(additionalWearersData, (address[])); @@ -853,9 +878,104 @@ 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) { @@ -905,7 +1025,6 @@ contract RunOrgActionsAdvanced is Script { hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: role.hierarchy.adminRoleIndex}), distribution: RoleConfigStructs.RoleDistributionConfig({ mintToDeployer: role.distribution.mintToDeployer, - mintToExecutor: role.distribution.mintToExecutor, additionalWearers: role.distribution.additionalWearers }), hatConfig: RoleConfigStructs.HatConfig({ @@ -955,6 +1074,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-advanced-demo.json b/script/org-config-advanced-demo.json index c2980ee..39d1026 100644 --- a/script/org-config-advanced-demo.json +++ b/script/org-config-advanced-demo.json @@ -26,7 +26,6 @@ }, "distribution": { "mintToDeployer": false, - "mintToExecutor": false, "additionalWearers": [] }, "hatConfig": { @@ -53,7 +52,6 @@ }, "distribution": { "mintToDeployer": false, - "mintToExecutor": false, "additionalWearers": [] }, "hatConfig": { @@ -80,7 +78,6 @@ }, "distribution": { "mintToDeployer": false, - "mintToExecutor": false, "additionalWearers": [] }, "hatConfig": { @@ -107,7 +104,6 @@ }, "distribution": { "mintToDeployer": true, - "mintToExecutor": false, "additionalWearers": [] }, "hatConfig": { diff --git a/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/HatsTreeSetup.sol b/src/HatsTreeSetup.sol index 2e1b04c..85c3c28 100644 --- a/src/HatsTreeSetup.sol +++ b/src/HatsTreeSetup.sol @@ -161,10 +161,13 @@ contract HatsTreeSetup { ); // Step 5: Collect all eligibility and toggle operations for batch execution - // Count total eligibility entries needed: 2 per role (executor + deployer) + additional wearers + // Count total eligibility entries needed: deployer (only if minting) + additional wearers uint256 eligibilityCount = 0; for (uint256 i = 0; i < len; i++) { - eligibilityCount += 2; // executor + deployer + // Deployer only eligible if they're receiving the hat (matches minting conditions) + if (params.roles[i].canVote && params.roles[i].distribution.mintToDeployer) { + eligibilityCount += 1; + } eligibilityCount += params.roles[i].distribution.additionalWearers.length; } @@ -186,14 +189,12 @@ contract HatsTreeSetup { uint256 hatId = result.roleHatIds[i]; RoleConfigStructs.RoleConfig memory role = params.roles[i]; - // Collect eligibility entries - eligWearers[eligIndex] = params.executor; - eligHatIds[eligIndex] = hatId; - eligIndex++; - - eligWearers[eligIndex] = params.deployerAddress; - eligHatIds[eligIndex] = hatId; - eligIndex++; + // Deployer only eligible if they're receiving the hat (matches minting conditions) + if (role.canVote && role.distribution.mintToDeployer) { + eligWearers[eligIndex] = params.deployerAddress; + eligHatIds[eligIndex] = hatId; + eligIndex++; + } // Collect additional wearers for (uint256 j = 0; j < role.distribution.additionalWearers.length; j++) { @@ -226,7 +227,6 @@ contract HatsTreeSetup { if (!role.canVote) continue; if (role.distribution.mintToDeployer) mintCount++; - if (role.distribution.mintToExecutor) mintCount++; mintCount += role.distribution.additionalWearers.length; } @@ -255,12 +255,6 @@ contract HatsTreeSetup { mintIndex++; } - if (role.distribution.mintToExecutor) { - hatIdsToMint[mintIndex] = hatId; - wearersToMint[mintIndex] = params.executor; - mintIndex++; - } - for (uint256 j = 0; j < role.distribution.additionalWearers.length; j++) { hatIdsToMint[mintIndex] = hatId; wearersToMint[mintIndex] = role.distribution.additionalWearers[j]; diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index 73656aa..ca6225d 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -43,6 +43,35 @@ 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); + + function clearDeployer() external; +} + /** * @title OrgDeployer * @notice Thin orchestrator for deploying complete organizations using factory pattern @@ -79,6 +108,11 @@ contract OrgDeployer is Initializable { bytes32 indexed orgId, uint256[] hatIds, string[] names, string[] images, bytes32[] metadataCIDs, bool[] canVote ); + /// @notice Emitted after OrgDeployed to provide initial wearer assignments for subgraph indexing + event InitialWearersAssigned( + bytes32 indexed orgId, address indexed eligibilityModule, address[] wearers, uint256[] hatIds + ); + /*───────────── ERC-7201 Storage ───────────*/ /// @custom:storage-location erc7201:poa.orgdeployer.storage struct Layout { @@ -175,6 +209,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 +230,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 +409,18 @@ 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); + } + + /* 8.6. Clear deployer address to prevent future bootstrap calls (defense-in-depth) */ + ITaskManagerBootstrap(result.taskManager).clearDeployer(); + /* 9. Authorize QuickJoin to mint hats */ IExecutorAdmin(result.executor).setHatMinterAuthorization(result.quickJoin, true); @@ -426,6 +478,16 @@ contract OrgDeployer is Initializable { gov.roleHatIds ); + /* 12b. Emit initial wearer assignments for subgraph User creation */ + { + (address[] memory wearers, uint256[] memory hatIds) = + _collectInitialWearers(params.roles, gov.roleHatIds, params.deployerAddress); + + if (wearers.length > 0) { + emit InitialWearersAssigned(params.orgId, gov.eligibilityModule, wearers, hatIds); + } + } + /* 13. Emit role metadata for subgraph indexing */ { uint256 roleCount = params.roles.length; @@ -454,6 +516,45 @@ contract OrgDeployer is Initializable { return exists; } + /** + * @notice Collects all initial wearers from role configurations + * @dev Used to emit InitialWearersAssigned event for subgraph indexing + */ + function _collectInitialWearers( + RoleConfigStructs.RoleConfig[] calldata roles, + uint256[] memory roleHatIds, + address deployerAddress + ) internal pure returns (address[] memory wearers, uint256[] memory hatIds) { + // First pass: count total wearers + uint256 totalCount = 0; + for (uint256 i = 0; i < roles.length; i++) { + if (!roles[i].canVote) continue; + if (roles[i].distribution.mintToDeployer) totalCount++; + totalCount += roles[i].distribution.additionalWearers.length; + } + + // Second pass: populate arrays + wearers = new address[](totalCount); + hatIds = new uint256[](totalCount); + uint256 idx = 0; + + for (uint256 i = 0; i < roles.length; i++) { + if (!roles[i].canVote) continue; + uint256 hatId = roleHatIds[i]; + + if (roles[i].distribution.mintToDeployer) { + wearers[idx] = deployerAddress; + hatIds[idx] = hatId; + idx++; + } + for (uint256 j = 0; j < roles[i].distribution.additionalWearers.length; j++) { + wearers[idx] = roles[i].distribution.additionalWearers[j]; + hatIds[idx] = hatId; + idx++; + } + } + } + /** * @notice Internal helper to deploy governance infrastructure * @dev Extracted to reduce stack depth in main deployment function @@ -585,4 +686,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 58a9248..f79fd86 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,70 @@ 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; + } + } + } + + /** + * @notice Clear the deployer address after bootstrap phase is complete + * @dev Only callable by deployer. Prevents future bootstrap calls for defense-in-depth. + * Should be called by OrgDeployer at the end of org deployment. + */ + function clearDeployer() external { + Layout storage l = _layout(); + if (_msgSender() != l.deployer) revert NotDeployer(); + l.deployer = address(0); + } + /*──────── Task Logic ───────*/ function createTask( uint256 payout, diff --git a/src/factories/ModulesFactory.sol b/src/factories/ModulesFactory.sol index 5d35687..c40c0e5 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 76e9f3e..acb8082 100644 --- a/src/libs/ModuleDeploymentLib.sol +++ b/src/libs/ModuleDeploymentLib.sol @@ -63,7 +63,8 @@ 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 +195,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/src/libs/RoleConfigStructs.sol b/src/libs/RoleConfigStructs.sol index 9311de4..a3d0f98 100644 --- a/src/libs/RoleConfigStructs.sol +++ b/src/libs/RoleConfigStructs.sol @@ -34,7 +34,6 @@ library RoleConfigStructs { /// @dev Controls who gets the role minted to them initially struct RoleDistributionConfig { bool mintToDeployer; // Mint to deployer address - bool mintToExecutor; // Mint to executor contract address[] additionalWearers; // Additional addresses to mint to } diff --git a/test/DeployerTest.t.sol b/test/DeployerTest.t.sol index 104ae24..d7aefae 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); @@ -214,9 +215,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { adminRoleIndex: isTopRole ? type(uint256).max : i + 1 }), distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: isTopRole && canVote[i], - mintToExecutor: !isTopRole && canVote[i], - additionalWearers: new address[](0) + mintToDeployer: isTopRole && canVote[i], additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({ maxSupply: type(uint32).max, // Default: unlimited @@ -247,6 +246,64 @@ 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 bootstrap config with one project and two tasks + function _buildBootstrapWithTasks() internal pure returns (OrgDeployer.BootstrapConfig memory) { + ITaskManagerBootstrap.BootstrapProjectConfig[] memory projects = + new ITaskManagerBootstrap.BootstrapProjectConfig[](1); + + uint256[] memory createRoles = new uint256[](1); + createRoles[0] = 1; // EXECUTIVE role index + uint256[] memory claimRoles = new uint256[](2); + claimRoles[0] = 0; // DEFAULT role index + claimRoles[1] = 1; // EXECUTIVE role index + uint256[] memory reviewRoles = new uint256[](1); + reviewRoles[0] = 1; // EXECUTIVE role index + uint256[] memory assignRoles = new uint256[](1); + assignRoles[0] = 1; // EXECUTIVE role index + address[] memory managers = new address[](0); + + projects[0] = ITaskManagerBootstrap.BootstrapProjectConfig({ + title: bytes("Getting Started"), + metadataHash: bytes32(0), + cap: 1000 ether, + managers: managers, + createHats: createRoles, + claimHats: claimRoles, + reviewHats: reviewRoles, + assignHats: assignRoles + }); + + ITaskManagerBootstrap.BootstrapTaskConfig[] memory tasks = new ITaskManagerBootstrap.BootstrapTaskConfig[](2); + tasks[0] = ITaskManagerBootstrap.BootstrapTaskConfig({ + projectIndex: 0, + payout: 10 ether, + title: bytes("Complete your profile"), + metadataHash: bytes32(0), + bountyToken: address(0), + bountyPayout: 0, + requiresApplication: false + }); + tasks[1] = ITaskManagerBootstrap.BootstrapTaskConfig({ + projectIndex: 0, + payout: 5 ether, + title: bytes("Introduce yourself"), + metadataHash: bytes32(0), + bountyToken: address(0), + bountyPayout: 0, + requiresApplication: false + }); + + return OrgDeployer.BootstrapConfig({projects: projects, tasks: tasks}); + } + /// @dev Helper to build legacy-style voting classes function _buildLegacyClasses(uint8 ddSplit, uint8 ptSplit, bool quadratic, uint256 minBal) internal @@ -347,7 +404,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 +460,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 +787,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); @@ -822,6 +882,86 @@ contract DeployerTest is Test, IEligibilityModuleEvents { assertEq(paymentManager.owner(), exec, "PaymentManager owner should be executor"); } + function testFullOrgDeploymentWithBootstrapAndClearDeployer() public { + /*–––– deploy a full org with bootstrap config ––––*/ + vm.startPrank(orgOwner); + + string[] memory names = new string[](2); + names[0] = "DEFAULT"; + names[1] = "EXECUTIVE"; + string[] memory images = new string[](2); + images[0] = "ipfs://default-role-image"; + images[1] = "ipfs://executive-role-image"; + bool[] memory voting = new bool[](2); + voting[0] = true; + voting[1] = true; + + IHybridVotingInit.ClassConfig[] memory classes = _buildLegacyClasses(50, 50, false, 4 ether); + OrgDeployer.RoleAssignments memory roleAssignments = _buildDefaultRoleAssignments(); + address[] memory ddTargets = new address[](0); + + OrgDeployer.DeploymentParams memory params = OrgDeployer.DeploymentParams({ + orgId: ORG_ID, + orgName: "Bootstrap DAO", + metadataHash: bytes32(0), + registryAddr: accountRegProxy, + deployerAddress: orgOwner, + deployerUsername: "", + autoUpgrade: true, + hybridQuorumPct: 50, + ddQuorumPct: 50, + hybridClasses: classes, + ddInitialTargets: ddTargets, + roles: _buildSimpleRoleConfigs(names, images, voting), + roleAssignments: roleAssignments, + passkeyEnabled: false, + educationHubConfig: ModulesFactory.EducationHubConfig({enabled: true}), + bootstrap: _buildBootstrapWithTasks() + }); + + OrgDeployer.DeploymentResult memory result = deployer.deployFullOrg(params); + + vm.stopPrank(); + + // Verify TaskManager was deployed + assertTrue(result.taskManager != address(0), "TaskManager should be deployed"); + + // After deployment, deployer address should be cleared + // Attempting to call bootstrap again should fail with NotDeployer + ITaskManagerBootstrap.BootstrapProjectConfig[] memory moreProjects = + new ITaskManagerBootstrap.BootstrapProjectConfig[](1); + + uint256[] memory createRoles = new uint256[](1); + createRoles[0] = 1; + uint256[] memory claimRoles = new uint256[](1); + claimRoles[0] = 0; + address[] memory managers = new address[](0); + + moreProjects[0] = ITaskManagerBootstrap.BootstrapProjectConfig({ + title: bytes("Second Project"), + metadataHash: bytes32(0), + cap: 100 ether, + managers: managers, + createHats: createRoles, + claimHats: claimRoles, + reviewHats: createRoles, + assignHats: createRoles + }); + + ITaskManagerBootstrap.BootstrapTaskConfig[] memory moreTasks = + new ITaskManagerBootstrap.BootstrapTaskConfig[](0); + + // OrgDeployer should no longer be able to bootstrap (deployer was cleared) + vm.prank(address(deployer)); + vm.expectRevert(TaskManager.NotDeployer.selector); + ITaskManagerBootstrap(result.taskManager).bootstrapProjectsAndTasks(moreProjects, moreTasks); + + // clearDeployer should also fail since deployer is already cleared + vm.prank(address(deployer)); + vm.expectRevert(TaskManager.NotDeployer.selector); + ITaskManagerBootstrap(result.taskManager).clearDeployer(); + } + function testDeployFullOrgMismatchExecutorReverts() public { _deployFullOrg(); address other = address(99); @@ -857,7 +997,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 +1036,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 +1118,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 +1387,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 +1600,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 +1761,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 +1896,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); @@ -1772,7 +1919,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { defaults: RoleConfigStructs.RoleEligibilityDefaults({eligible: true, standing: true}), hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: type(uint256).max}), distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: false, mintToExecutor: true, additionalWearers: new address[](0) + mintToDeployer: false, additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({maxSupply: type(uint32).max, mutableHat: true}) }); @@ -1787,7 +1934,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { defaults: RoleConfigStructs.RoleEligibilityDefaults({eligible: true, standing: true}), hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: type(uint256).max}), distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: true, mintToExecutor: false, additionalWearers: new address[](0) + mintToDeployer: true, additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({maxSupply: type(uint32).max, mutableHat: true}) }); @@ -1812,7 +1959,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { defaults: RoleConfigStructs.RoleEligibilityDefaults({eligible: true, standing: true}), hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: type(uint256).max}), distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: true, mintToExecutor: false, additionalWearers: new address[](0) + mintToDeployer: true, additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({maxSupply: type(uint32).max, mutableHat: true}) }); @@ -1834,7 +1981,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { defaults: RoleConfigStructs.RoleEligibilityDefaults({eligible: true, standing: true}), hierarchy: RoleConfigStructs.RoleHierarchyConfig({adminRoleIndex: 0}), // Self-reference distribution: RoleConfigStructs.RoleDistributionConfig({ - mintToDeployer: true, mintToExecutor: false, additionalWearers: new address[](0) + mintToDeployer: true, additionalWearers: new address[](0) }), hatConfig: RoleConfigStructs.HatConfig({maxSupply: type(uint32).max, mutableHat: true}) }); @@ -2023,7 +2170,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 +2274,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 +2439,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 +3421,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 +3520,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 +3580,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 +3642,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 9cc1fd2..a7c903a 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,491 @@ 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"); + } + + /*───────────────────────────────────────────────────────────────────────────── + clearDeployer Tests + ─────────────────────────────────────────────────────────────────────────────*/ + + function test_ClearDeployerSuccess() public { + // deployer can clear themselves + vm.prank(deployer); + tm.clearDeployer(); + + // After clearing, deployer should no longer be able to bootstrap + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Should Fail", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](0); + + vm.prank(deployer); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(projects, tasks); + } + + function test_ClearDeployerOnlyDeployer() public { + // Non-deployer cannot clear deployer + vm.prank(creator1); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.clearDeployer(); + + vm.prank(executor); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.clearDeployer(); + + vm.prank(outsider); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.clearDeployer(); + } + + function test_ClearDeployerCannotBeCalledTwice() public { + // First clear succeeds + vm.prank(deployer); + tm.clearDeployer(); + + // Second clear fails (deployer is now address(0)) + vm.prank(deployer); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.clearDeployer(); + } + + function test_BootstrapThenClear() public { + // Bootstrap first + TaskManager.BootstrapProjectConfig[] memory projects = new TaskManager.BootstrapProjectConfig[](1); + projects[0] = _buildBootstrapProject( + "Initial Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + TaskManager.BootstrapTaskConfig[] memory tasks = new TaskManager.BootstrapTaskConfig[](1); + tasks[0] = _buildBootstrapTask(0, "Initial Task", 10 ether); + + vm.prank(deployer); + bytes32[] memory projectIds = tm.bootstrapProjectsAndTasks(projects, tasks); + assertEq(projectIds.length, 1, "Should create 1 project"); + + // Clear deployer + vm.prank(deployer); + tm.clearDeployer(); + + // Cannot bootstrap again + TaskManager.BootstrapProjectConfig[] memory moreProjects = new TaskManager.BootstrapProjectConfig[](1); + moreProjects[0] = _buildBootstrapProject( + "Second Project", + 100 ether, + _hatArr(CREATOR_HAT), + _hatArr(MEMBER_HAT), + _hatArr(PM_HAT), + _hatArr(PM_HAT) + ); + TaskManager.BootstrapTaskConfig[] memory moreTasks = new TaskManager.BootstrapTaskConfig[](0); + + vm.prank(deployer); + vm.expectRevert(TaskManager.NotDeployer.selector); + tm.bootstrapProjectsAndTasks(moreProjects, moreTasks); + } + }