ghost-dao-contracts/src/Gatekeeper.sol

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;
}
}