// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; 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 {Checkpoints} from "./libraries/Checkpoints.sol"; import {RotationPacking, RequestPacking} 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; uint256 private constant COMMISSION_DIVISOR = type(uint32).max; uint256 private constant SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; uint256 private constant SECP256K1_Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; uint256 private constant SHIFT_FACTOR = (2**128) % SECP256K1_N; address public override staking; address public override storageHistory; uint256 public override existentialDeposit; address public deployer; address private _previousAddress; Checkpoints.Trace256 private _aggregatedPublicKeys; mapping(bytes32 => uint256) private _packedRotationStates; constructor( uint256 _existentialDeposit, address _previousGatekeeperAddress ) Weaver(_previousGatekeeperAddress) { existentialDeposit = _existentialDeposit; if (_previousGatekeeperAddress != address(0)) { require(_previousGatekeeperAddress != address(0)); require(_previousGatekeeperAddress != address(this)); _previousAddress = _previousGatekeeperAddress; storageHistory = IGatekeeper(_previousGatekeeperAddress).storageHistory(); staking = IGatekeeper(_previousGatekeeperAddress).staking(); } else { StorageHistory newStorageHistory = new StorageHistory(msg.sender); storageHistory = address(newStorageHistory); staking = msg.sender; deployer = tx.origin; } } 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 override 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 verify( bytes calldata call, bytes calldata nonce, bytes32 s ) external nonReentrant payable returns (bytes memory) { _verifySchnorrSignature(call, nonce, s); (bool success, bytes memory data) = address(this).call{ value: msg.value }(call); if (!success) revert ExecutionReverted(); return data; } function ghost(bytes32 receiver, uint256 amount) external override returns (uint256) { if (msg.sender != staking) revert NotStaking(); if (amount < existentialDeposit) revert NonExistentAmount(); IStorageHistory(storageHistory).increaseBridgeIn(amount); emit Ghosted(receiver, amount); return _insertTreeNode(receiver, amount); } function materialize( uint256 exodusSession, uint256 amount, uint256 packed ) external override payable { if (msg.sender != address(this)) revert NotGatekeeper(); RequestPacking.RequestPayload memory payload = RequestPacking.unpack(packed); if (payload.chainId != block.chainid) revert WrongChainId(); StorageHistory(storageHistory).trySetTransactionExecuted(exodusSession); IStorageHistory(storageHistory).tryIncreaseBridgeOut(amount); uint256 commissionAmount = FullMath.mulDiv(amount, uint256(payload.commission), COMMISSION_DIVISOR); uint256 receiverAmount = amount - commissionAmount; IStaking(staking).materialize(payload.receiver, receiverAmount); if (commissionAmount > 0) { uint256 totalReserves = IStaking(staking).totalReserves(); uint256 baseSupply = IStaking(staking).baseSupply(); uint256 minimumNativeRequired = FullMath.mulDiv(commissionAmount, totalReserves, baseSupply); if (minimumNativeRequired > msg.value) revert InsufficientValue(); IStaking(staking).materialize(tx.origin, commissionAmount); (bool sentSuccess,) = payload.receiver.call{ value: msg.value }(""); if (!sentSuccess) revert SendFailed(); } emit Materialized(payload.receiver, amount); } function rotate( uint256 exodusSession, bytes32 newPublicKey, uint8 newParity ) external override { if (msg.sender != address(this)) revert NotGatekeeper(); 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 _verifySchnorrSignature(bytes calldata call, bytes calldata nonce, bytes32 s) internal view { (bytes32 px, uint8 p) = _extractPublicKey(call); uint8 v = p == 0 ? 27 : 28; bytes32 e = _challenge(call, nonce, px, p); bytes32 sp = bytes32(SECP256K1_Q - mulmod(uint256(s), uint256(px), SECP256K1_Q)); bytes32 ep = bytes32(SECP256K1_Q - mulmod(uint256(e), uint256(px), SECP256K1_Q)); address recoveredR = ecrecover(sp, v, px, ep); if (sp == 0) revert BadSignature(); if (recoveredR == address(0)) revert BadSignature(); if (recoveredR != _uncompressedPointToAddress(nonce)) revert BadSignature(); } function _uncompressedPointToAddress(bytes calldata nonce) internal pure returns (address) { if (nonce.length != 65) revert BadSignature(); if (nonce[0] != 0x04) revert BadSignature(); bytes32 rx = bytes32(nonce[1:33]); bytes32 ry = bytes32(nonce[33:65]); return address(uint160(uint256(keccak256(abi.encodePacked(rx, ry))))); } function _extractPublicKey(bytes calldata call) internal view returns (bytes32, uint8) { if (call.length < 36) revert InvalidCalldata(); uint256 exodusSession; assembly { exodusSession := calldataload(add(call.offset, 4)) } (bytes32 publicKey, uint8 parity,) = getRotationInfoAt(exodusSession); return (publicKey, parity); } function _challenge( bytes calldata call, bytes calldata r, bytes32 pk, uint8 v ) internal pure returns (bytes32) { if (r.length != 65) revert BadSignature(); bytes32 rx = bytes32(r[1:33]); bytes1 rv; { bytes32 ry = bytes32(r[33:65]); uint8 rParityByte = (uint256(ry) & 1) == 0 ? 0x02 : 0x03; rv = bytes1(rParityByte); } uint8 pv = v == 0 ? 0x02 : 0x03; bytes memory message = abi.encodePacked(rv, rx, pv, pk, call); bytes memory domain = "FROST-secp256k1-SHA256-v1chal"; uint8 dstlen = uint8(domain.length); bytes32 b0 = sha256(abi.encodePacked( bytes32(0), bytes32(0), message, hex"0030", uint8(0), domain, dstlen )); bytes32 b1 = sha256(abi.encodePacked(b0, uint8(1), domain, dstlen)); bytes32 b2 = sha256(abi.encodePacked(b0 ^ b1, uint8(2), domain, dstlen)); uint256 high32 = uint256(b1); uint256 lowPart = uint256(b2) >> 128; uint256 highReduced = mulmod(high32, SHIFT_FACTOR, SECP256K1_N); uint256 finalChallenge = addmod(highReduced, lowPart, SECP256K1_N); return bytes32(finalChallenge); } }