// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IWETH9} from "./interfaces/IWETH9.sol"; import {IStaking} from "./interfaces/IStaking.sol"; import {IGatekeeper} from "./interfaces/IGatekeeper.sol"; import {IStorageHistory} from "./interfaces/IStorageHistory.sol"; import {StorageHistory} from "./types/StorageHistory.sol"; import {Weaver} from "./types/Weaver.sol"; import {FullMath} from "./libraries/FullMath.sol"; import {Verifier} from "./libraries/Verifier.sol"; import {Checkpoints} from "./libraries/Checkpoints.sol"; import {RotationPacking, RequestPacking, GovernancePacking} from "./libraries/Packing.sol"; import {ReentrancyGuard} from "@openzeppelin-contracts/utils/ReentrancyGuard.sol"; contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { using Checkpoints for Checkpoints.Trace256; using RotationPacking for RotationPacking.RotationState; using RequestPacking for RequestPacking.RequestPayload; using GovernancePacking for GovernancePacking.GovernancePayload; uint256 public constant BOUNTY_DIVISOR = type(uint32).max; uint256 public constant EXISTENTIAL_DEPOSIT = 500 * 1e12; // 0.005 uint256 public constant FULL_GAS_RESTORATION_TIME = 3600; // 1 hour uint256 public constant MAX_ALLOWED_GAS_PRICE = 3e9; // 3 gwei uint256 public constant GAS_EXECUTION_BUFFER = 21000 + 5969 + 50955; uint256 public constant REGISTRY_INDEX = 0; address public override staking; address public override deployer; address public override storageHistory; uint256 private _lastVerificationTime; address private _previousAddress; bool private _initialized; Checkpoints.Trace256 private _aggregatedPublicKeys; mapping(bytes32 => uint256) private _packedRotationStates; constructor(address _storageHistory, address _staking) { storageHistory = _storageHistory; staking = _staking; deployer = tx.origin; } receive() external payable {} function initialize(address _previousGatekeeperAddress) external override { require(msg.sender == staking); if (_previousGatekeeperAddress != address(0)) { require(_initialized == false); require(_previousGatekeeperAddress != address(this)); address previousStorage = IGatekeeper(_previousGatekeeperAddress).storageHistory(); require(previousStorage != address(0)); storageHistory = previousStorage; deployer = IGatekeeper(_previousGatekeeperAddress).deployer(); Weaver._initialize(_previousGatekeeperAddress); _previousAddress = _previousGatekeeperAddress; } _initialized = true; } function updatePublicKeyMetadata(uint256 exodusSession, bytes32 publicKey, uint8 parity) external { require(msg.sender == deployer); // TODO: uncomment line below, needed only for testing purposes // and should push to hardcoded exodusSession 0 always // _deployer = address(0); // forge-lint: disable-next-line(unsafe-typecast) uint256 packedRotationState = RotationPacking.pack(parity, uint64(exodusSession)); _aggregatedPublicKeys.push(exodusSession, uint256(publicKey)); _packedRotationStates[publicKey] = packedRotationState; } function previousAddress() external override view returns (address) { return _previousAddress; } function ghostedSupply() external override view returns (uint256) { return IStorageHistory(storageHistory).bridgeImbalance(); } function latestPublicKeyInfo() external view returns (bytes32, uint8, uint64) { bytes32 latestPublicKey = bytes32(_aggregatedPublicKeys.latest()); uint256 packed = _packedRotationStates[latestPublicKey]; RotationPacking.RotationState memory state = RotationPacking.unpack(packed); return (latestPublicKey, state.parity, state.session); } function getRotationInfoAt(uint256 exodusSession) public override view returns (bytes32, uint8, uint64) { bytes32 publicKey = bytes32(_aggregatedPublicKeys.upperLookup(exodusSession)); uint256 packed = _packedRotationStates[publicKey]; RotationPacking.RotationState memory state = RotationPacking.unpack(packed); if (exodusSession < state.session) { return IGatekeeper(_previousAddress).getRotationInfoAt(exodusSession); } return (publicKey, state.parity, state.session); } function getCurrentGasPrice() public view returns (uint256) { uint256 timePassed = block.timestamp - _lastVerificationTime; if (timePassed >= FULL_GAS_RESTORATION_TIME) { return MAX_ALLOWED_GAS_PRICE; } return FullMath.mulDiv(MAX_ALLOWED_GAS_PRICE, timePassed, FULL_GAS_RESTORATION_TIME); } function ghost(bytes32 receiver, uint256 amount) external override { if (msg.sender != staking) revert NotStaking(); if (amount < EXISTENTIAL_DEPOSIT) revert NonExistentAmount(); IStorageHistory(storageHistory).increaseBridgeIn(amount); _insertTreeNode(receiver, amount); } function verify( bytes calldata call, uint256 rx, uint256 s ) external nonReentrant { uint256 gasStart = gasleft(); uint256 px = _extractPublicKey(call); bool validSignature = Verifier.verifyGhost(call, px, rx, s); if (!validSignature) revert BadSignature(); bytes4 selector = bytes4(call[:4]); if ( selector != this.recall.selector && selector != this.rotate.selector && selector != this.govern.selector ) { revert InvalidSelector(); } (bool success,) = address(this).call(call); if (!success) revert ExecutionReverted(); uint256 currentGasPrice = getCurrentGasPrice(); _lastVerificationTime = block.timestamp; uint256 gasSpent = (gasStart - gasleft() + GAS_EXECUTION_BUFFER) * currentGasPrice; try IStaking(staking).phantomRefund(msg.sender, gasSpent, REGISTRY_INDEX) {} catch { emit VoluntaryVerification(msg.sender, gasSpent); } } function recall( uint256 exodusSession, uint256 amount, uint256 packed ) external { if (msg.sender != address(this)) revert NotGatekeeper(); RequestPacking.RequestPayload memory payload = RequestPacking.unpack(packed); if (payload.chainId != block.chainid) revert WrongChainId(); uint256 requestedBountyAmount = FullMath.mulDiv(amount, uint256(payload.bounty), BOUNTY_DIVISOR); uint256 bountyAmount = _isContract(payload.receiver) ? 0 : requestedBountyAmount; uint256 receiverAmount = amount - bountyAmount; StorageHistory(storageHistory).trySetTransactionExecuted(exodusSession); IStorageHistory(storageHistory).tryIncreaseBridgeOut(receiverAmount); IStaking(staking).recall(payload.receiver, receiverAmount); if (bountyAmount > 0) { (address token, uint256 sent) = IStaking(staking).recall(bountyAmount, REGISTRY_INDEX); IWETH9(token).withdraw(sent); (bool sentSuccess,) = payload.receiver.call{ value: sent, gas: 3000 }(""); if (!sentSuccess) revert SendFailed(); } emit Recalled(payload.receiver, amount); } function rotate( uint256 exodusSession, bytes32 newPublicKey, uint8 newParity ) external { if (msg.sender != address(this)) revert NotGatekeeper(); if (newParity % 2 != 0) revert InvalidPublicKey(); StorageHistory(storageHistory).trySetTransactionExecuted(exodusSession); // forge-lint: disable-next-line(unsafe-typecast) uint256 packedRotationState = RotationPacking.pack(newParity, uint64(exodusSession)); _aggregatedPublicKeys.push(exodusSession, uint256(newPublicKey)); _packedRotationStates[newPublicKey] = packedRotationState; emit Rotated(newPublicKey, newParity); } function govern( uint256 exodusSession, uint256 packed, bytes calldata call ) external { if (msg.sender != address(this)) revert NotGatekeeper(); GovernancePacking.GovernancePayload memory payload = GovernancePacking.unpack(packed); if (payload.chainId != block.chainid) revert WrongChainId(); StorageHistory(storageHistory).trySetTransactionExecuted(exodusSession); (bool success,) = payload.target.call(call); if (!success) revert ExecutionReverted(); } function _extractPublicKey(bytes calldata call) internal view returns (uint256) { if (call.length < 36) revert InvalidCalldata(); uint256 exodusSession; assembly { exodusSession := calldataload(add(call.offset, 4)) } (bytes32 publicKey,,) = getRotationInfoAt(exodusSession); return uint256(publicKey); } function _isContract(address account) internal view returns (bool) { uint256 size; assembly { size := extcodesize(account) } return size > 0; } }