// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { Test } from "forge-std/Test.sol"; import { MultisigGovernanceV3 } from "../../contracts/governance/MultisigGovernanceV3.sol"; import { BasePilotAccessControllerV3, BasePilotAccessControllerCoreV3 } from "../../contracts/pilot/BasePilotAccessControllerV3.sol"; import { DisputeRegistryV3 } from "../../contracts/disputes/DisputeRegistryV3.sol"; import { TaskEscrowV3 } from "../../contracts/escrow/TaskEscrowV3.sol"; import { MutualAidPoolV3 } from "../../contracts/mutualaid/MutualAidPoolV3.sol"; import { ERC8004ValidationRegistryV3Safe } from "../../contracts/reputation/ERC8004ValidationRegistryV3Safe.sol"; contract PilotModuleMock { bytes32 public constant supportedPilotModuleId = keccak256("Vaultfire.BaseV3.SafeBondFactory"); address public immutable pilotController; constructor(address controller) { pilotController = controller; } function register( BasePilotAccessControllerV3 controller, address designPartner, address position, uint256 initialAllocation ) external { controller.registerPosition(designPartner, position, initialAllocation); } function increase(BasePilotAccessControllerV3 controller, address position, uint256 amount) external { controller.increasePositionAllocation(position, amount); } function release(BasePilotAccessControllerV3 controller, address position, uint256 amount) external { controller.releasePositionAllocation(position, amount); } } contract BasePilotAccessControllerV3Test is Test { address internal constant SIGNER_A = address(0xA11CE); address internal constant SIGNER_B = address(0xB0B); address internal constant GUARDIAN = address(0x600D); address internal constant DESIGN_PARTNER = address(0xD3516); address internal constant ROTATED_DESIGN_PARTNER = address(0xD3517); address internal constant POSITION_A = address(0xA001); address internal constant POSITION_B = address(0xB001); address internal constant POSITION_C = address(0xC001); uint256 internal constant TIMELOCK = 1 days; uint256 internal constant MAX_GLOBAL_CAP = 1_000; uint256 internal constant MAX_PER_POSITION_CAP = 400; MultisigGovernanceV3 internal governance; BasePilotAccessControllerV3 internal controller; PilotModuleMock internal module; function setUp() external { vm.chainId(8453); address[] memory signers = new address[](2); signers[0] = SIGNER_A; signers[1] = SIGNER_B; address[] memory noTargets = new address[](0); uint64 nonce = vm.getNonce(address(this)); address predictedGovernance = vm.computeCreateAddress(address(this), nonce + 1); BasePilotAccessControllerV3 referenceController = new BasePilotAccessControllerV3(predictedGovernance, MAX_GLOBAL_CAP, MAX_PER_POSITION_CAP); address[] memory canonicalTargets = new address[](1); canonicalTargets[0] = vm.computeCreateAddress(address(this), nonce + 2); bytes32[] memory canonicalHashes = new bytes32[](1); canonicalHashes[0] = address(referenceController).codehash; governance = new MultisigGovernanceV3( signers, 2, TIMELOCK, TIMELOCK, GUARDIAN, TIMELOCK, noTargets, canonicalTargets, canonicalHashes ); controller = new BasePilotAccessControllerV3(address(governance), MAX_GLOBAL_CAP, MAX_PER_POSITION_CAP); assertEq(address(controller), canonicalTargets[0]); module = new PilotModuleMock(address(controller)); vm.startPrank(address(governance)); governance.pinCanonicalAddress(address(module), address(module).codehash); governance.setCallPolicy( address(controller), controller.setDesignPartner.selector, true, 0, address(controller).codehash ); governance.setCallPolicy( address(controller), controller.setModule.selector, true, 0, address(controller).codehash ); governance.setCallPolicy( address(controller), controller.setCaps.selector, true, 0, address(controller).codehash ); governance.setCallPolicy( address(controller), controller.setDeployEnabled.selector, true, 0, address(controller).codehash ); governance.setCallPolicy( address(controller), controller.setWriteEnabled.selector, true, 0, address(controller).codehash ); vm.stopPrank(); } function testIsBaseOnlyAndDefaultsToClosed() external { assertFalse(controller.deployEnabled()); assertFalse(controller.writeEnabled()); assertEq(controller.totalAllocated(), 0); vm.chainId(1); vm.expectRevert(BasePilotAccessControllerCoreV3.UnsupportedChain.selector); new BasePilotAccessControllerV3(address(governance), MAX_GLOBAL_CAP, MAX_PER_POSITION_CAP); } function testOnlyGovernanceCanConfigurePilot() external { vm.expectRevert(BasePilotAccessControllerCoreV3.NotGovernance.selector); controller.setDesignPartner(DESIGN_PARTNER, true); _govern(abi.encodeCall(controller.setDesignPartner, (DESIGN_PARTNER, true))); _govern(abi.encodeCall(controller.setModule, (address(module), true))); _govern(abi.encodeCall(controller.setCaps, (MAX_GLOBAL_CAP, MAX_PER_POSITION_CAP))); assertTrue(controller.isDesignPartner(DESIGN_PARTNER)); assertTrue(controller.isModule(address(module))); assertEq(controller.globalCap(), MAX_GLOBAL_CAP); assertEq(controller.perPositionCap(), MAX_PER_POSITION_CAP); assertEq(controller.maxGlobalCap(), MAX_GLOBAL_CAP); assertEq(controller.maxPerPositionCap(), MAX_PER_POSITION_CAP); assertEq(controller.designPartnerCount(), 1); } function testPilotAllowsExactlyOneDesignPartnerAndSupportsExplicitRotation() external { _govern(abi.encodeCall(controller.setDesignPartner, (DESIGN_PARTNER, true))); _governExpectCallFailure(abi.encodeCall(controller.setDesignPartner, (ROTATED_DESIGN_PARTNER, true))); assertTrue(controller.isDesignPartner(DESIGN_PARTNER)); assertFalse(controller.isDesignPartner(ROTATED_DESIGN_PARTNER)); assertEq(controller.designPartnerCount(), 1); _govern(abi.encodeCall(controller.setDesignPartner, (DESIGN_PARTNER, false))); _govern(abi.encodeCall(controller.setDesignPartner, (ROTATED_DESIGN_PARTNER, true))); assertFalse(controller.isDesignPartner(DESIGN_PARTNER)); assertTrue(controller.isDesignPartner(ROTATED_DESIGN_PARTNER)); assertEq(controller.designPartnerCount(), 1); } function testClosedPilotRejectsDeploysAndWritesUntilGovernanceEnablesEachPath() external { _configure(1_000, 400); vm.expectRevert(BasePilotAccessControllerCoreV3.DeployDisabled.selector); module.register(controller, DESIGN_PARTNER, POSITION_A, 100); _govern(abi.encodeCall(controller.setDeployEnabled, (true))); module.register(controller, DESIGN_PARTNER, POSITION_A, 100); vm.expectRevert(BasePilotAccessControllerCoreV3.WriteDisabled.selector); module.increase(controller, POSITION_A, 1); _govern(abi.encodeCall(controller.setWriteEnabled, (true))); module.increase(controller, POSITION_A, 50); assertEq(controller.positionAllocated(POSITION_A), 150); assertEq(controller.totalAllocated(), 150); } function testEnforcesAllowlistGlobalAndPerPositionCaps() external { _configure(1_000, 400); _govern(abi.encodeCall(controller.setDeployEnabled, (true))); _govern(abi.encodeCall(controller.setWriteEnabled, (true))); vm.expectRevert(BasePilotAccessControllerCoreV3.DesignPartnerNotAllowed.selector); module.register(controller, address(0xBAD), POSITION_A, 1); vm.expectRevert(BasePilotAccessControllerCoreV3.PositionCapExceeded.selector); module.register(controller, DESIGN_PARTNER, POSITION_A, 401); module.register(controller, DESIGN_PARTNER, POSITION_A, 400); assertEq(controller.positionCap(POSITION_A), 400); vm.expectRevert(BasePilotAccessControllerCoreV3.PositionCapExceeded.selector); module.increase(controller, POSITION_A, 1); module.register(controller, DESIGN_PARTNER, POSITION_B, 400); vm.expectRevert(BasePilotAccessControllerCoreV3.GlobalCapExceeded.selector); module.register(controller, DESIGN_PARTNER, POSITION_C, 201); assertEq(controller.totalAllocated(), 800); } function testReleaseRemainsAvailableWhenWritesAndDeploysAreDisabled() external { _configure(1_000, 400); _govern(abi.encodeCall(controller.setDeployEnabled, (true))); module.register(controller, DESIGN_PARTNER, POSITION_A, 300); _govern(abi.encodeCall(controller.setDeployEnabled, (false))); _govern(abi.encodeCall(controller.setWriteEnabled, (false))); module.release(controller, POSITION_A, 125); assertEq(controller.positionAllocated(POSITION_A), 175); assertEq(controller.totalAllocated(), 175); } function testOnlyApprovedModulesCanUseHooksAndCapsCannotDropBelowAllocatedValue() external { _configure(1_000, 400); _govern(abi.encodeCall(controller.setDeployEnabled, (true))); vm.expectRevert(BasePilotAccessControllerCoreV3.NotModule.selector); controller.registerPosition(DESIGN_PARTNER, POSITION_A, 1); module.register(controller, DESIGN_PARTNER, POSITION_A, 300); _governExpectCallFailure(abi.encodeCall(controller.setCaps, (299, 400))); } function testConstructorRejectsZeroOrInconsistentImmutableMaximumCaps() external { vm.expectRevert(BasePilotAccessControllerCoreV3.InvalidMaximumCap.selector); new BasePilotAccessControllerV3(address(governance), 0, 1); vm.expectRevert(BasePilotAccessControllerCoreV3.InvalidMaximumCap.selector); new BasePilotAccessControllerV3(address(governance), 1, 0); vm.expectRevert(BasePilotAccessControllerCoreV3.InvalidMaximumCap.selector); new BasePilotAccessControllerV3(address(governance), 1, 2); } function test_retiredModulesCannotBeControllerAuthorized() external { address[4] memory retired = [ address(new DisputeRegistryV3(address(governance))), address(new TaskEscrowV3(address(governance), address(1), 0, 1, 1, 1)), address(new MutualAidPoolV3(address(governance), address(0), 1, 1)), address(new ERC8004ValidationRegistryV3Safe(address(governance), address(1), 1, 1 ether, 10 ether)) ]; for (uint256 i; i < retired.length; ++i) { vm.expectRevert(BasePilotAccessControllerCoreV3.UnsupportedModule.selector); vm.prank(address(governance)); controller.setModule(retired[i], true); } } function testGovernanceCanLowerCapsButCannotExceedImmutableMaximums() external { _configure(900, 300); assertEq(controller.globalCap(), 900); assertEq(controller.perPositionCap(), 300); _govern(abi.encodeCall(controller.setCaps, (MAX_GLOBAL_CAP, MAX_PER_POSITION_CAP))); assertEq(controller.globalCap(), MAX_GLOBAL_CAP); assertEq(controller.perPositionCap(), MAX_PER_POSITION_CAP); _governExpectCallFailure(abi.encodeCall(controller.setCaps, (MAX_GLOBAL_CAP + 1, MAX_PER_POSITION_CAP))); _governExpectCallFailure(abi.encodeCall(controller.setCaps, (MAX_GLOBAL_CAP, MAX_PER_POSITION_CAP + 1))); _governExpectCallFailure(abi.encodeCall(controller.setCaps, (399, 400))); } function _configure(uint256 globalCap, uint256 perPositionCap) internal { _govern(abi.encodeCall(controller.setDesignPartner, (DESIGN_PARTNER, true))); _govern(abi.encodeCall(controller.setModule, (address(module), true))); _govern(abi.encodeCall(controller.setCaps, (globalCap, perPositionCap))); } function _govern(bytes memory data) internal { vm.prank(SIGNER_A); uint256 id = governance.propose(address(controller), 0, data); vm.prank(SIGNER_A); governance.confirm(id); vm.prank(SIGNER_B); governance.confirm(id); vm.warp(block.timestamp + TIMELOCK); vm.prank(SIGNER_A); governance.execute(id); } function _governExpectCallFailure(bytes memory data) internal { vm.expectPartialRevert(MultisigGovernanceV3.PolicyDenied.selector); vm.prank(SIGNER_A); governance.propose(address(controller), 0, data); } }