diff --git a/src/Gatekeeper.sol b/src/Gatekeeper.sol new file mode 100644 index 0000000..da57cc8 --- /dev/null +++ b/src/Gatekeeper.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./interfaces/IGatekeeper.sol"; + +contract Gatekeeper is IGatekeeper { + uint256 public override ghostedSupply; + address public immutable staking; + + uint256 private _aggregatedPublicKey; + mapping(uint256 => mapping(uint256 => bool)) private _executedTransaction; + + constructor( + address _staking, + uint256 _ghostedSupply + ) { + ghostedSupply = _ghostedSupply; + staking = _staking; + } + + function ghost(bytes32 receiver, uint256 amount) external override { + if (msg.sender != staking) revert NotStaking(); + ghostedSupply += amount; + emit Ghosted(receiver, amount); + } + + function materialize( + address receiver, + uint256 amount, + uint256 rx, + uint256 s + ) external override { + if (msg.sender != staking) revert NotStaking(); + _checkTransactionExistence(rx, s); + bytes32 message = keccak256(abi.encodePacked("materialize", receiver, amount)); + + if (_incorrectSignature(rx, s, message)) revert WrongSignature(); + ghostedSupply -= amount; + emit Materialized(receiver, amount); + } + + function rotate( + uint256 aggregatedPublicKey, + uint256 rx, + uint256 s + ) external override { + _checkTransactionExistence(rx, s); + bytes32 message = keccak256(abi.encodePacked("rotate", aggregatedPublicKey)); + + if (_incorrectSignature(rx, s, message)) revert WrongSignature(); + _aggregatedPublicKey = aggregatedPublicKey; + emit Rotated(aggregatedPublicKey); + } + + function _checkTransactionExistence(uint256 rx, uint256 s) private { + if (_executedTransaction[rx][s]) revert AlreadyExecuted(); + _executedTransaction[rx][s] = true; + } + + function _incorrectSignature(uint256 rx, uint256 s, bytes32 m) private pure returns (bool) { + // no logic below, just to suppress warnings from solc + uint256 void = rx; + void = s; + void = uint256(m); + + // always bad signature for now + return true; + } +} diff --git a/src/Staking.sol b/src/Staking.sol index 1593d4f..3e2d13d 100644 --- a/src/Staking.sol +++ b/src/Staking.sol @@ -10,6 +10,7 @@ import "./interfaces/ISTNK.sol"; import "./interfaces/IGHST.sol"; import "./interfaces/IDistributor.sol"; import "./interfaces/IStaking.sol"; +import "./interfaces/IGatekeeper.sol"; contract GhostStaking is IStaking, GhostAccessControlled { using SafeERC20 for IERC20; @@ -24,6 +25,7 @@ contract GhostStaking is IStaking, GhostAccessControlled { Epoch public epoch; address public distributor; + address public gatekeeper; uint256 public sharesInWarmup; mapping(address => Claim) public warmupInfo; @@ -133,6 +135,22 @@ contract GhostStaking is IStaking, GhostAccessControlled { ISTNK(stnk).safeTransfer(to, balance); } + function ghost( + bytes32 receiver, + uint256 amount + ) external override { + IGatekeeper(gatekeeper).ghost(receiver, amount); + } + + function materialize( + address receiver, + uint256 amount, + uint256 rx, + uint256 s + ) external override { + IGatekeeper(gatekeeper).materialize(receiver, amount, rx, s); + } + function rebase() public override returns (uint256 bounty) { if (epoch.end <= block.timestamp) { ISTNK(stnk).rebase(epoch.distribute, epoch.number); @@ -164,6 +182,11 @@ contract GhostStaking is IStaking, GhostAccessControlled { emit WarmupSet(_warmupPeriod); } + function setGatekeeperAddress(address _gatekeeper) external onlyGovernor { + gatekeeper = _gatekeeper; + emit GatekeeperSet(_gatekeeper); + } + function index() public view override returns (uint256) { return ISTNK(stnk).index(); } @@ -172,6 +195,12 @@ contract GhostStaking is IStaking, GhostAccessControlled { return ISTNK(stnk).balanceForShares(sharesInWarmup); } + function ghostedSupply() public view override returns (uint256 amount) { + if (gatekeeper != address(0)) { + amount = IGatekeeper(gatekeeper).ghostedSupply(); + } + } + function _send( uint256 amount, address to, diff --git a/src/StinkyERC20.sol b/src/StinkyERC20.sol index 1d92631..49ba7a9 100644 --- a/src/StinkyERC20.sol +++ b/src/StinkyERC20.sol @@ -168,7 +168,7 @@ contract Stinky is ISTNK, ERC20Permit { function circulatingSupply() public view override returns (uint256) { return _totalSupply + - IGHST(ghst).balanceFrom(IERC20(ghst).totalSupply()) + + IGHST(ghst).balanceFrom(IERC20(ghst).totalSupply() + IStaking(staking).ghostedSupply()) + IStaking(staking).supplyInWarmup() - balanceOf(staking); } diff --git a/src/interfaces/IGatekeeper.sol b/src/interfaces/IGatekeeper.sol new file mode 100644 index 0000000..9b93bdf --- /dev/null +++ b/src/interfaces/IGatekeeper.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IGatekeeper { + error NotStaking(); + error WrongSignature(); + error AlreadyExecuted(); + + event Ghosted(bytes32 indexed receiver, uint256 indexed amount); + event Materialized(address indexed receiver, uint256 indexed amount); + event Rotated(uint256 indexed aggregatedPublicKey); + + function ghostedSupply() external view returns (uint256); + function ghost(bytes32 receiver, uint256 amount) external; + function materialize(address receiver, uint256 amount, uint256 rx, uint256 s) external; + function rotate(uint256 aggregatedPublicKey, uint256 rx, uint256 s) external; +} diff --git a/src/interfaces/IStaking.sol b/src/interfaces/IStaking.sol index fe43a9e..1ea7e93 100644 --- a/src/interfaces/IStaking.sol +++ b/src/interfaces/IStaking.sol @@ -7,6 +7,7 @@ interface IStaking { error InsufficientBalance(); event DistributorSet(address distributor); + event GatekeeperSet(address gatekeeper); event WarmupSet(uint256 warmup); struct Epoch { @@ -43,7 +44,11 @@ interface IStaking { function wrap(address _to, uint256 _amount) external returns (uint256 gBalance_); function unwrap(address _to, uint256 _amount) external returns (uint256 sBalance_); + function ghost(bytes32 receiver, uint256 amount) external; + function materialize(address receiver, uint256 amount, uint256 rx, uint256 s) external; function rebase() external returns (uint256); + function index() external view returns (uint256); function supplyInWarmup() external view returns (uint256); + function ghostedSupply() external view returns (uint256); }