diff --git a/src/Gatekeeper.sol b/src/Gatekeeper.sol index 6155575..9ee33d6 100644 --- a/src/Gatekeeper.sol +++ b/src/Gatekeeper.sol @@ -10,12 +10,15 @@ 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 = 2**32; + 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; @@ -28,7 +31,7 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { address private _previousAddress; Checkpoints.Trace256 private _aggregatedPublicKeys; - mapping(bytes32 => RotationState) private _publicKeyMetadatas; + mapping(bytes32 => uint256) private _packedRotationStates; constructor( uint256 _existentialDeposit, @@ -54,17 +57,14 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { 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 in any case + // and should push to hardcoded exodusSession 0 always // deployer = address(0); - RotationState memory rotationState = RotationState({ - parity: parity, - // forge-lint: disable-next-line(unsafe-typecast) - session: uint64(exodusSession) - }); + // forge-lint: disable-next-line(unsafe-typecast) + uint256 packedRotationState = RotationPacking.pack(parity, uint64(exodusSession)); _aggregatedPublicKeys.push(exodusSession, uint256(publicKey)); - _publicKeyMetadatas[publicKey] = rotationState; + _packedRotationStates[publicKey] = packedRotationState; } function previousAddress() external override view returns (address) { @@ -77,40 +77,32 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { function latestPublicKeyInfo() external override view returns (bytes32, uint8, uint64) { bytes32 latestPublicKey = bytes32(_aggregatedPublicKeys.latest()); - RotationState memory state = _publicKeyMetadatas[latestPublicKey]; + 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)); - RotationState memory rotationState = _publicKeyMetadatas[publicKey]; + uint256 packed = _packedRotationStates[publicKey]; + RotationPacking.RotationState memory state = RotationPacking.unpack(packed); - if (exodusSession < rotationState.session) { + if (exodusSession < state.session) { return IGatekeeper(_previousAddress).getRotationInfoAt(exodusSession); } - return (publicKey, rotationState.parity, rotationState.session); + return (publicKey, state.parity, state.session); } function verify( bytes calldata call, bytes calldata nonce, bytes32 s - ) external nonReentrant returns (bytes memory) { - (bytes32 px, uint8 p) = _extractPublicKey(call); + ) external nonReentrant payable returns (bytes memory) { + _verifySchnorrSignature(call, nonce, s); - 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(); - - (bool success, bytes memory data) = address(this).call(call); + (bool success, bytes memory data) = address(this).call{ value: msg.value }(call); if (!success) revert ExecutionReverted(); return data; @@ -129,23 +121,34 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { function materialize( uint256 exodusSession, uint256 amount, - uint256 commission, - address receiver - ) external override { + 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, commission, COMMISSION_DIVISOR); + uint256 commissionAmount = FullMath.mulDiv(amount, uint256(payload.commission), COMMISSION_DIVISOR); uint256 receiverAmount = amount - commissionAmount; - IStaking(staking).materialize(receiver, receiverAmount); + + IStaking(staking).materialize(payload.receiver, receiverAmount); if (commissionAmount > 0) { - IStaking(staking).materialize(tx.origin, commission); + 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(receiver, amount); + emit Materialized(payload.receiver, amount); } function rotate( @@ -156,18 +159,30 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard { if (msg.sender != address(this)) revert NotGatekeeper(); StorageHistory(storageHistory).trySetTransactionExecuted(exodusSession); - RotationState memory rotationState = RotationState({ - parity: newParity, - // forge-lint: disable-next-line(unsafe-typecast) - session: uint64(exodusSession) - }); + // forge-lint: disable-next-line(unsafe-typecast) + uint256 packedRotationState = RotationPacking.pack(newParity, uint64(exodusSession)); _aggregatedPublicKeys.push(exodusSession, uint256(newPublicKey)); - _publicKeyMetadatas[newPublicKey] = rotationState; + _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(); diff --git a/src/Staking.sol b/src/Staking.sol index 80efc1c..7b08838 100644 --- a/src/Staking.sol +++ b/src/Staking.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {GhostWarmup} from "./Warmup.sol"; import {Gatekeeper} from "./Gatekeeper.sol"; @@ -11,11 +11,12 @@ import {GhostAccessControlled} from "./types/GhostAccessControlled.sol"; import {ISTNK} from "./interfaces/ISTNK.sol"; import {IGHST} from "./interfaces/IGHST.sol"; import {IStaking} from "./interfaces/IStaking.sol"; +import {ITreasury} from "./interfaces/ITreasury.sol"; import {IGatekeeper} from "./interfaces/IGatekeeper.sol"; import {IDistributor} from "./interfaces/IDistributor.sol"; +import {IGhostWarmup} from "./interfaces/IGhostWarmup.sol"; import {IStorageHistory} from "./interfaces/IStorageHistory.sol"; import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol"; -import {IGhostWarmup} from "./interfaces/IGhostWarmup.sol"; contract GhostStaking is IStaking, GhostAccessControlled { using SafeERC20 for IERC20; @@ -228,6 +229,15 @@ contract GhostStaking is IStaking, GhostAccessControlled { return (deposit, payout, expiry, lock); } + function totalReserves() external override view returns (uint256) { + address treasury = ISTNK(stnk).treasury(); + return ITreasury(treasury).totalReserves(); + } + + function baseSupply() external override view returns (uint256) { + return IERC20(ftso).totalSupply(); + } + function _sendStnkBased( uint256 amount, address to, diff --git a/src/interfaces/IGatekeeper.sol b/src/interfaces/IGatekeeper.sol index e57972a..787e81a 100644 --- a/src/interfaces/IGatekeeper.sol +++ b/src/interfaces/IGatekeeper.sol @@ -3,18 +3,16 @@ pragma solidity ^0.8.20; interface IGatekeeper { error NotStaking(); - error NotGatekeeper(); + error SendFailed(); + error WrongChainId(); error BadSignature(); + error NotGatekeeper(); error AlreadyExecuted(); - error NonExistentAmount(); error InvalidCalldata(); + error NonExistentAmount(); + error InsufficientValue(); error ExecutionReverted(); - struct RotationState { - uint8 parity; - uint64 session; - } - event Ghosted(bytes32 indexed receiver, uint256 indexed amount); event Materialized(address indexed receiver, uint256 indexed amount); event Rotated(bytes32 indexed aggregatedPublicKey, uint8 indexed parity); @@ -26,7 +24,7 @@ interface IGatekeeper { function latestPublicKeyInfo() external view returns (bytes32, uint8, uint64); function getRotationInfoAt(uint256 session) external view returns (bytes32, uint8, uint64); - function materialize(uint256 session, uint256 amount, uint256 commission, address receiver) external; + function materialize(uint256 session, uint256 amount, uint256 payload) external payable; function rotate(uint256 session, bytes32 publicKey, uint8 parity) external; function ghost(bytes32 receiver, uint256 amount) external returns (uint256); } diff --git a/src/interfaces/ISTNK.sol b/src/interfaces/ISTNK.sol index 5571b08..8637627 100644 --- a/src/interfaces/ISTNK.sol +++ b/src/interfaces/ISTNK.sol @@ -18,6 +18,7 @@ interface ISTNK is IERC20 { function increaseAllowance(address spender, uint256 imbalance) external returns (bool); function decreaseAllowance(address spender, uint256 imbalance) external returns (bool); function circulatingSupply() external view returns (uint256); + function treasury() external view returns (address); function sharesForBalance(uint256 amount) external view returns (uint256); function balanceForShares(uint256 shares) external view returns (uint256); function index() external view returns (uint256); diff --git a/src/interfaces/IStaking.sol b/src/interfaces/IStaking.sol index e64a9c5..18a958d 100644 --- a/src/interfaces/IStaking.sol +++ b/src/interfaces/IStaking.sol @@ -74,4 +74,6 @@ interface IStaking { function index() external view returns (uint256); function supplyInWarmup() external view returns (uint256); function ghostedSupply() external view returns (uint256); + function totalReserves() external view returns (uint256); + function baseSupply() external view returns (uint256); } diff --git a/src/interfaces/ITreasury.sol b/src/interfaces/ITreasury.sol index 292f61d..2688e43 100644 --- a/src/interfaces/ITreasury.sol +++ b/src/interfaces/ITreasury.sol @@ -65,5 +65,6 @@ interface ITreasury { function indexInRegistry(address _address, STATUS _status) external view returns (bool, uint256); function excessReserves() external view returns (uint256); function baseSupply() external view returns (uint256); + function totalReserves() external view returns (uint256); } diff --git a/src/libraries/Packing.sol b/src/libraries/Packing.sol new file mode 100644 index 0000000..ff96ca4 --- /dev/null +++ b/src/libraries/Packing.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +library RotationPacking { + struct RotationState { + uint8 parity; + uint64 session; + } + + function pack(uint8 parity, uint64 session) internal pure returns (uint256 packed) { + // forge-lint: disable-next-line(unsafe-typecast) + return (uint256(session) << 8) | uint256(parity); + } + + function pack(RotationState calldata state) internal pure returns (uint256 packed) { + return pack(state.parity, state.session); + } + + function unpack(uint256 packed) internal pure returns (RotationState memory state) { + // forge-lint: disable-next-line(unsafe-typecast) + state.parity = uint8(packed & 0xFF); + state.session = uint64((packed >> 8) & 0xFFFFFFFFFFFFFFFF); + return state; + } +} + +library RequestPacking { + struct RequestPayload { + uint32 commission; + uint64 chainId; + address receiver; + } + + function pack(uint32 commission, uint64 chainId, address receiver) internal pure returns (uint256 packed) { + packed = uint256(uint160(receiver)) << 96; + packed |= uint256(chainId) << 32; + packed |= uint256(commission); + return packed; + } + + function pack(RequestPayload calldata data) internal pure returns (uint256 packed) { + return pack(data.commission, data.chainId, data.receiver); + } + + function unpack(uint256 packed) internal pure returns (RequestPayload memory data) { + // forge-lint: disable-next-line(unsafe-typecast) + data.commission = uint32(packed & 0xFFFFFFFF); + // forge-lint: disable-next-line(unsafe-typecast) + data.chainId = uint64((packed >> 32) & 0xFFFFFFFFFFFFFFFF); + // forge-lint: disable-next-line(unsafe-typecast) + data.receiver = address(uint160(packed >> 96)); + return data; + } +} diff --git a/src/types/StorageHistory.sol b/src/types/StorageHistory.sol index 83aa351..54a8ee3 100644 --- a/src/types/StorageHistory.sol +++ b/src/types/StorageHistory.sol @@ -44,7 +44,6 @@ contract StorageHistory is IStorageHistory { function increaseBridgeIn(uint256 amount) external override { if (msg.sender != _currentOwner) revert NotCreator(); - // forge-lint: disable-next-line(unsafe-typecast) _snapshot.amountIn += uint104(amount); } @@ -52,7 +51,6 @@ contract StorageHistory is IStorageHistory { function tryIncreaseBridgeOut(uint256 amount) external override { if (msg.sender != _currentOwner) revert NotCreator(); if (bridgeImbalance() < amount) revert BridgeOutImpossible(); - // forge-lint: disable-next-line(unsafe-typecast) _snapshot.amountOut += uint104(amount); }