packages/contracts-rfq/contracts/AdminV2.sol
// 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);
}
}