253 lines
9.5 KiB
Solidity
253 lines
9.5 KiB
Solidity
// 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);
|
|
}
|
|
}
|