// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ import {IAdminV2} from "./interfaces/IAdminV2.sol"; import {IAdminV2Errors} from "./interfaces/IAdminV2Errors.sol"; // ═════════════════════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════════════════════════ import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; /// @title AdminV2 /// @notice Provides administrative functions and controls for managing the FastBridgeV2 contract, /// including access control and configuration settings. contract AdminV2 is AccessControlEnumerable, IAdminV2, IAdminV2Errors { using SafeERC20 for IERC20; /// @notice Struct for storing information about a prover. /// @param id The ID of the prover: its position in `_allProvers` plus one, /// or zero if the prover has never been added. /// @param activeFromTimestamp The timestamp at which the prover becomes active, /// or zero if the prover has never been added or is no longer active. struct ProverInfo { uint16 id; uint240 activeFromTimestamp; } /// @notice The address reserved for the native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc.). address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// @notice The role identifier for the Quoter API's off-chain authentication. /// @dev Only addresses with this role can post FastBridge quotes to the API. bytes32 public constant QUOTER_ROLE = keccak256("QUOTER_ROLE"); /// @notice The role identifier for the Guard's on-chain authentication in FastBridge. /// @dev Only addresses with this role can dispute submitted relay proofs during the dispute period. bytes32 public constant GUARD_ROLE = keccak256("GUARD_ROLE"); /// @notice The role identifier for the Canceler's on-chain authentication in FastBridge. /// @dev Only addresses with this role can cancel a FastBridge transaction without the cancel delay. bytes32 public constant CANCELER_ROLE = keccak256("CANCELER_ROLE"); /// @notice The role identifier for the Governor's on-chain administrative authority. /// @dev Only addresses with this role can perform administrative tasks within the contract. bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE"); /// @notice The denominator for fee rates, representing 100%. uint256 public constant FEE_BPS = 1e6; /// @notice The maximum protocol fee rate: 1% of the origin amount. uint256 public constant FEE_RATE_MAX = 0.01e6; /// @notice The minimum cancel delay that can be set by the governor. uint256 public constant MIN_CANCEL_DELAY = 1 hours; /// @notice The default cancel delay set during contract deployment. uint256 public constant DEFAULT_CANCEL_DELAY = 1 days; /// @notice The minimum dispute penalty time that can be set by the governor. uint256 public constant MIN_DISPUTE_PENALTY_TIME = 1 minutes; /// @notice The default dispute penalty time set during contract deployment. uint256 public constant DEFAULT_DISPUTE_PENALTY_TIME = 30 minutes; /// @notice The protocol fee rate taken on the origin amount deposited in the origin chain. uint256 public protocolFeeRate; /// @notice The accumulated protocol fee amounts. mapping(address => uint256) public protocolFees; /// @notice The delay period after which a transaction can be permissionlessly cancelled. uint256 public cancelDelay; /// @notice The block number at which the contract was deployed. /// @dev This used to be immutable in V1, but was adjusted to be mutable in V2 for chains like Arbitrum that /// implement the `block.number` as the underlying L1 block number rather than the chain's native block number. /// This is exposed for conveniece for off-chain indexers that need to know the deployment block. uint256 public deployBlock; /// @notice The timeout period that is used to temporarily disactivate a disputed prover. uint256 public disputePenaltyTime; /// @notice A list of all provers ever added to the contract. Can hold up to 2^16-1 provers. address[] private _allProvers; /// @notice A mapping of provers to their information: id and activeFromTimestamp. mapping(address => ProverInfo) private _proverInfos; /// @notice This variable is deprecated and should not be used. /// @dev Use ZapNative V2 requests instead. uint256 public immutable chainGasAmount = 0; constructor(address defaultAdmin) { _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); _setCancelDelay(DEFAULT_CANCEL_DELAY); _setDeployBlock(block.number); _setDisputePenaltyTime(DEFAULT_DISPUTE_PENALTY_TIME); } /// @inheritdoc IAdminV2 function addProver(address prover) external onlyRole(DEFAULT_ADMIN_ROLE) { if (getActiveProverID(prover) != 0) revert ProverAlreadyActive(); ProverInfo storage $ = _proverInfos[prover]; // Add the prover to the list of all provers and record its id (its position + 1), // if this has not already been done. if ($.id == 0) { _allProvers.push(prover); uint256 id = _allProvers.length; if (id > type(uint16).max) revert ProverCapacityExceeded(); // Note: this is a storage write. $.id = uint16(id); } // Update the activeFrom timestamp. // Note: this is a storage write. $.activeFromTimestamp = uint240(block.timestamp); emit ProverAdded(prover); } /// @inheritdoc IAdminV2 function removeProver(address prover) external onlyRole(DEFAULT_ADMIN_ROLE) { if (getActiveProverID(prover) == 0) revert ProverNotActive(); // We never remove provers from the list of all provers to preserve their IDs, // so we just need to reset the activeFrom timestamp. _proverInfos[prover].activeFromTimestamp = 0; emit ProverRemoved(prover); } /// @inheritdoc IAdminV2 function setCancelDelay(uint256 newCancelDelay) external onlyRole(GOVERNOR_ROLE) { _setCancelDelay(newCancelDelay); } /// @inheritdoc IAdminV2 function setDeployBlock(uint256 blockNumber) external onlyRole(DEFAULT_ADMIN_ROLE) { _setDeployBlock(blockNumber); } /// @inheritdoc IAdminV2 function setDisputePenaltyTime(uint256 newDisputePenaltyTime) external onlyRole(GOVERNOR_ROLE) { _setDisputePenaltyTime(newDisputePenaltyTime); } /// @inheritdoc IAdminV2 function setProtocolFeeRate(uint256 newFeeRate) external onlyRole(GOVERNOR_ROLE) { if (newFeeRate > FEE_RATE_MAX) revert FeeRateAboveMax(); uint256 oldFeeRate = protocolFeeRate; protocolFeeRate = newFeeRate; emit FeeRateUpdated(oldFeeRate, newFeeRate); } /// @inheritdoc IAdminV2 function sweepProtocolFees(address token, address recipient) external onlyRole(GOVERNOR_ROLE) { // Early exit if no accumulated fees. uint256 feeAmount = protocolFees[token]; if (feeAmount == 0) return; // Reset the accumulated fees first. protocolFees[token] = 0; emit FeesSwept(token, recipient, feeAmount); // Sweep the fees as the last transaction action. if (token == NATIVE_GAS_TOKEN) { Address.sendValue(payable(recipient), feeAmount); } else { IERC20(token).safeTransfer(recipient, feeAmount); } } /// @inheritdoc IAdminV2 function getProvers() external view returns (address[] memory provers) { uint256 length = _allProvers.length; // Calculate the number of active provers. uint256 activeProversCount = 0; for (uint256 i = 0; i < length; i++) { if (getActiveProverID(_allProvers[i]) != 0) { activeProversCount++; } } // Do the second pass to populate the provers array. provers = new address[](activeProversCount); uint256 activeProversIndex = 0; for (uint256 i = 0; i < length; i++) { address prover = _allProvers[i]; if (getActiveProverID(prover) != 0) { provers[activeProversIndex++] = prover; } } } /// @inheritdoc IAdminV2 function getProverInfo(address prover) external view returns (uint16 proverID, uint256 activeFromTimestamp) { proverID = _proverInfos[prover].id; activeFromTimestamp = _proverInfos[prover].activeFromTimestamp; } /// @inheritdoc IAdminV2 function getProverInfoByID(uint16 proverID) external view returns (address prover, uint256 activeFromTimestamp) { if (proverID == 0 || proverID > _allProvers.length) return (address(0), 0); prover = _allProvers[proverID - 1]; activeFromTimestamp = _proverInfos[prover].activeFromTimestamp; } /// @inheritdoc IAdminV2 function getActiveProverID(address prover) public view returns (uint16) { // Aggregate the read operations from the same storage slot. uint16 id = _proverInfos[prover].id; uint256 activeFromTimestamp = _proverInfos[prover].activeFromTimestamp; // Return zero if the prover has never been added or is no longer active. if (activeFromTimestamp == 0 || activeFromTimestamp > block.timestamp) return 0; return id; } /// @notice Internal logic to apply the dispute penalty time to a given prover. Will make the prover inactive /// for `disputePenaltyTime` seconds. No-op if the prover ID does not exist or prover is already inactive. function _applyDisputePenaltyTime(uint16 proverID) internal { // Check that the prover exists. if (proverID == 0 || proverID > _allProvers.length) return; address prover = _allProvers[proverID - 1]; ProverInfo storage $ = _proverInfos[prover]; // No-op if the prover is already inactive. if ($.activeFromTimestamp == 0) return; uint256 newActiveFromTimestamp = block.timestamp + disputePenaltyTime; // Update the activeFrom timestamp. // Note: this is a storage write. $.activeFromTimestamp = uint240(newActiveFromTimestamp); emit DisputePenaltyTimeApplied(prover, newActiveFromTimestamp); } /// @notice Internal logic to set the cancel delay. Security checks are performed outside of this function. /// @dev This function is marked as private to prevent child contracts from calling it directly. function _setCancelDelay(uint256 newCancelDelay) private { if (newCancelDelay < MIN_CANCEL_DELAY) revert CancelDelayBelowMin(); uint256 oldCancelDelay = cancelDelay; cancelDelay = newCancelDelay; emit CancelDelayUpdated(oldCancelDelay, newCancelDelay); } /// @notice Internal logic to set the deploy block. Security checks are performed outside of this function. /// @dev This function is marked as private to prevent child contracts from calling it directly. function _setDeployBlock(uint256 blockNumber) private { deployBlock = blockNumber; emit DeployBlockSet(blockNumber); } /// @notice Internal logic to set the dispute penalty time. Security checks are performed outside of this function. /// @dev This function is marked as private to prevent child contracts from calling it directly. function _setDisputePenaltyTime(uint256 newDisputePenaltyTime) private { if (newDisputePenaltyTime < MIN_DISPUTE_PENALTY_TIME) revert DisputePenaltyTimeBelowMin(); uint256 oldDisputePenaltyTime = disputePenaltyTime; disputePenaltyTime = newDisputePenaltyTime; emit DisputePenaltyTimeUpdated(oldDisputePenaltyTime, newDisputePenaltyTime); } }