245 lines
9.0 KiB
Solidity
245 lines
9.0 KiB
Solidity
// 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;
|
|
}
|
|
}
|