// SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title BasePilotAccessControllerCoreV3 /// @notice Shared immutable-chain pilot gate for factories and modules. /// @dev This contract holds no assets and exposes no withdrawal path. Modules /// opt in by registering positions before accepting pilot value and by /// releasing value after redemptions. Releasing is intentionally allowed /// even while new deploys and writes are disabled. /// @dev This is an implementation detail. Deploy only one of the explicitly /// named profiles below; callers must never select a chain at deployment time. abstract contract BasePilotAccessControllerCoreV3 { bytes32 public constant SUPPORTED_PILOT_MODULE_ID = keccak256("Vaultfire.BaseV3.SafeBondFactory"); bytes32 public constant GOVERNANCE_POLICY_MODULE_ID = keccak256("Vaultfire.BaseV3.PilotController"); uint256 public immutable requiredChainId; /// @dev Matches the governance pause-target maximum, so every marked child /// can be independently covered before writes are enabled. uint256 public constant MAX_GUARDIAN_COVERED_POSITIONS = 32; address public immutable governance; /// @notice Fresh-deployment liability ceiling for all registered positions. /// @dev This is intentionally immutable so governance may lower and later /// restore configured caps, but can never raise pilot exposure above /// the reviewed deployment-time maximum. uint256 public immutable maxGlobalCap; /// @notice Fresh-deployment liability ceiling for each registered position. uint256 public immutable maxPerPositionCap; bool public deployEnabled; bool public writeEnabled; uint256 public globalCap; uint256 public perPositionCap; uint256 public totalAllocated; uint256 public designPartnerCount; uint256 public moduleCount; mapping(address => bool) public isDesignPartner; mapping(address => bool) public isModule; mapping(address => address) public positionDesignPartner; mapping(address => uint256) public positionAllocated; mapping(address => uint256) public positionCap; mapping(address => bool) public guardianPauseCoverageRequired; address[] private _guardianCoveredPositions; error NotGovernance(); error NotModule(); error UnsupportedChain(); error ZeroAddress(); error InvalidCap(); error InvalidMaximumCap(); error DeployDisabled(); error WriteDisabled(); error DesignPartnerNotAllowed(); error PositionAlreadyRegistered(); error PositionNotRegistered(); error PositionCapExceeded(); error GlobalCapExceeded(); error AmountZero(); error AmountExceedsPositionAllocation(); error ModuleMustBeContract(); error DesignPartnerLimitReached(); /// @notice Controller writes cannot be enabled unless governance has /// registered the position as a guardian pause target. error GuardianPauseCoverageMissing(address position); error PositionMustBeContract(); error GuardianCoveredPositionLimitReached(); error UnsupportedModule(); event DesignPartnerSetV3(address indexed partner, bool allowed); event ModuleSetV3(address indexed module, bool allowed); event CapsSetV3(uint256 globalCap, uint256 perPositionCap); event DeployEnabledSetV3(bool enabled); event WriteEnabledSetV3(bool enabled); event PositionRegisteredV3( address indexed position, address indexed designPartner, uint256 initialAllocation, uint256 positionCap ); event PositionAllocationIncreasedV3(address indexed position, uint256 amount, uint256 newAllocation); event PositionAllocationReleasedV3(address indexed position, uint256 amount, uint256 newAllocation); modifier onlyGovernance() { if (msg.sender != governance) revert NotGovernance(); _; } modifier onlyModule() { if (!isModule[msg.sender]) revert NotModule(); _; } /// @dev A registered position may update only its own allocation. Approved /// modules retain the ability to operate positions they administer. /// This lets a factory register a freshly deployed position without /// granting that position blanket module authority. modifier onlyModuleOrPosition(address position) { if (msg.sender != position && !isModule[msg.sender]) revert NotModule(); _; } constructor(uint256 _requiredChainId, address _governance, uint256 _maxGlobalCap, uint256 _maxPerPositionCap) { if (block.chainid != _requiredChainId) revert UnsupportedChain(); if (_governance == address(0)) revert ZeroAddress(); if (_maxGlobalCap == 0 || _maxPerPositionCap == 0 || _maxPerPositionCap > _maxGlobalCap) { revert InvalidMaximumCap(); } requiredChainId = _requiredChainId; governance = _governance; maxGlobalCap = _maxGlobalCap; maxPerPositionCap = _maxPerPositionCap; } // --------------------------------------------------------------------- // Governance configuration // --------------------------------------------------------------------- function setDesignPartner(address partner, bool allowed) external onlyGovernance { if (partner == address(0)) revert ZeroAddress(); bool currentlyAllowed = isDesignPartner[partner]; if (currentlyAllowed == allowed) { emit DesignPartnerSetV3(partner, allowed); return; } if (allowed) { if (designPartnerCount != 0) revert DesignPartnerLimitReached(); designPartnerCount = 1; } else { designPartnerCount = 0; } isDesignPartner[partner] = allowed; emit DesignPartnerSetV3(partner, allowed); } function setModule(address module, bool allowed) external onlyGovernance { if (module == address(0)) revert ZeroAddress(); if (allowed) { if (module.code.length == 0) revert ModuleMustBeContract(); (bool idOk, bytes memory idData) = module.staticcall(abi.encodeWithSignature("supportedPilotModuleId()")); (bool controllerOk, bytes memory controllerData) = module.staticcall(abi.encodeWithSignature("pilotController()")); if ( !idOk || idData.length != 32 || abi.decode(idData, (bytes32)) != SUPPORTED_PILOT_MODULE_ID || !controllerOk || controllerData.length != 32 || abi.decode(controllerData, (address)) != address(this) ) revert UnsupportedModule(); } if (isModule[module] != allowed) { if (allowed) { ++moduleCount; } else { --moduleCount; } } isModule[module] = allowed; emit ModuleSetV3(module, allowed); } /// @notice Both caps must be configured before a module can register a /// position. The global cap cannot be set below current allocation /// or above the immutable fresh-deployment liability ceilings. function setCaps(uint256 newGlobalCap, uint256 newPerPositionCap) external onlyGovernance { if ( newGlobalCap == 0 || newPerPositionCap == 0 || newGlobalCap < totalAllocated || newGlobalCap > maxGlobalCap || newPerPositionCap > maxPerPositionCap || newPerPositionCap > newGlobalCap ) revert InvalidCap(); globalCap = newGlobalCap; perPositionCap = newPerPositionCap; emit CapsSetV3(newGlobalCap, newPerPositionCap); } function setDeployEnabled(bool enabled) external onlyGovernance { deployEnabled = enabled; emit DeployEnabledSetV3(enabled); } function setWriteEnabled(bool enabled) external onlyGovernance { if (enabled) _requireGuardianPauseCoverage(); writeEnabled = enabled; emit WriteEnabledSetV3(enabled); } // --------------------------------------------------------------------- // Factory / module hooks // --------------------------------------------------------------------- /// @notice Called by an approved factory or module after it has created a /// position and before it accepts the initial pilot allocation. function registerPosition(address designPartner, address position, uint256 initialAllocation) external onlyModule { _registerPosition(designPartner, position, initialAllocation); } /// @notice Registers a factory-created, value-bearing child position. /// @dev Unlike generic module positions, each child is subject to the /// guardian-pause proof checked before writes can be enabled. function registerGuardianPauseablePosition(address designPartner, address position, uint256 initialAllocation) external onlyModule { if (position.code.length == 0) revert PositionMustBeContract(); _registerPosition(designPartner, position, initialAllocation); if (_guardianCoveredPositions.length >= MAX_GUARDIAN_COVERED_POSITIONS) { revert GuardianCoveredPositionLimitReached(); } guardianPauseCoverageRequired[position] = true; _guardianCoveredPositions.push(position); } function _registerPosition(address designPartner, address position, uint256 initialAllocation) private { if (!deployEnabled) revert DeployDisabled(); if (!isDesignPartner[designPartner]) revert DesignPartnerNotAllowed(); if (position == address(0)) revert ZeroAddress(); if (positionDesignPartner[position] != address(0)) revert PositionAlreadyRegistered(); _checkAllocation(initialAllocation); positionDesignPartner[position] = designPartner; positionAllocated[position] = initialAllocation; positionCap[position] = perPositionCap; totalAllocated += initialAllocation; emit PositionRegisteredV3(position, designPartner, initialAllocation, perPositionCap); } /// @notice Called by an approved module before accepting additional value /// into a registered position. function increasePositionAllocation(address position, uint256 amount) external onlyModuleOrPosition(position) { if (!writeEnabled) revert WriteDisabled(); if (amount == 0) revert AmountZero(); address designPartner = positionDesignPartner[position]; if (designPartner == address(0)) revert PositionNotRegistered(); if (!isDesignPartner[designPartner]) revert DesignPartnerNotAllowed(); if (guardianPauseCoverageRequired[position] && !hasGuardianPauseCoverage(position)) { revert GuardianPauseCoverageMissing(position); } uint256 newAllocation = positionAllocated[position] + amount; if (newAllocation > positionCap[position]) revert PositionCapExceeded(); if (totalAllocated + amount > globalCap) revert GlobalCapExceeded(); positionAllocated[position] = newAllocation; totalAllocated += amount; emit PositionAllocationIncreasedV3(position, amount, newAllocation); } /// @notice Called by an approved module after a withdrawal, redemption, or /// settlement. This remains callable while the pilot is paused so /// the controller never impedes withdrawals. function releasePositionAllocation(address position, uint256 amount) external onlyModuleOrPosition(position) { if (amount == 0) revert AmountZero(); if (positionDesignPartner[position] == address(0)) revert PositionNotRegistered(); uint256 currentAllocation = positionAllocated[position]; if (amount > currentAllocation) revert AmountExceedsPositionAllocation(); uint256 newAllocation = currentAllocation - amount; positionAllocated[position] = newAllocation; totalAllocated -= amount; emit PositionAllocationReleasedV3(position, amount, newAllocation); } function _checkAllocation(uint256 allocation) private view { if (allocation > perPositionCap) revert PositionCapExceeded(); if (totalAllocated + allocation > globalCap) revert GlobalCapExceeded(); } /// @notice Reports whether governance objectively exposes guardian pause /// coverage for a registered position. /// @dev The controller deliberately reads the governance-owned, /// append-only pause-target registry rather than keeping a procedural /// local flag. An EOA, an incompatible governance contract, or a /// malformed response is fail-closed. function hasGuardianPauseCoverage(address position) public view returns (bool) { (bool ok, bytes memory result) = governance.staticcall(abi.encodeWithSignature("isGuardianPauseTarget(address)", position)); if (!ok || result.length < 32) return false; uint256 covered; assembly ("memory-safe") { covered := mload(add(result, 32)) } return covered == 1; } function _requireGuardianPauseCoverage() private view { for (uint256 i; i < _guardianCoveredPositions.length; ++i) { address position = _guardianCoveredPositions[i]; if (!hasGuardianPauseCoverage(position)) revert GuardianPauseCoverageMissing(position); } } } /// @title BasePilotAccessControllerV3 /// @notice Base mainnet (8453) pilot controller profile. /// @dev This profile remains mainnet-only. It cannot be used for Base Sepolia. contract BasePilotAccessControllerV3 is BasePilotAccessControllerCoreV3 { uint256 public constant BASE_CHAIN_ID = 8453; constructor(address _governance, uint256 _maxGlobalCap, uint256 _maxPerPositionCap) BasePilotAccessControllerCoreV3(BASE_CHAIN_ID, _governance, _maxGlobalCap, _maxPerPositionCap) { } }