// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; 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"; import {StorageHistory} from "./types/StorageHistory.sol"; import {GhostAccessControlled} from "./types/GhostAccessControlled.sol"; import {IFTSO} from "./interfaces/IFTSO.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"; contract GhostStaking is IStaking, GhostAccessControlled { using SafeERC20 for IERC20; using SafeERC20 for ISTNK; using SafeERC20 for IGHST; address public immutable ftso; // forge-lint: disable-line(screaming-snake-case-immutable) address public immutable stnk; // forge-lint: disable-line(screaming-snake-case-immutable) address public immutable ghst; // forge-lint: disable-line(screaming-snake-case-immutable) uint48 public warmupPeriod; Epoch public epoch; address public distributor; address public gatekeeper; address public warmup; uint256 private _lastRebaseBlock; mapping(address => bool) public unlocks; constructor( address _ftso, address _stnk, address _ghst, uint48 _epochLength, uint48 _firstEpochNumber, uint48 _firstEpochTime, address _authority ) GhostAccessControlled(IGhostAuthority(_authority)) { ftso = _ftso; stnk = _stnk; ghst = _ghst; epoch = Epoch({ length: _epochLength, number: _firstEpochNumber, end: _firstEpochTime, distribute: 0 }); GhostWarmup newWarmup = new GhostWarmup(_ghst); StorageHistory newHistory = new StorageHistory(); Gatekeeper newGatekeeper = new Gatekeeper(address(newHistory), address(this)); IStorageHistory(newHistory).setOwner(address(newGatekeeper)); IGatekeeper(newGatekeeper).initialize(address(0)); gatekeeper = address(newGatekeeper); warmup = address(newWarmup); _lastRebaseBlock = 1; } function stake( uint256 amount, address to, bool isRebase, bool isClaim ) external override returns (uint256 returnAmount) { if (!unlocks[to] && to != msg.sender) revert ExternalDepositsLocked(); returnAmount = amount + this.rebase(); IERC20(ftso).safeTransferFrom(msg.sender, address(this), amount); if (isClaim && warmupPeriod == 0) { returnAmount = _sendStnkBased(returnAmount, to, isRebase); } else { uint48 expiry = epoch.number + warmupPeriod; IGhostWarmup(warmup).addToWarmup(returnAmount, to, expiry); } emit Staked(msg.sender, to, amount, isRebase, isClaim); } function claim(address to, bool isRebase) public override returns (uint256) { if (!unlocks[to] && to != msg.sender) revert ExternalDepositsLocked(); uint256 claimedAmount = IGhostWarmup(warmup).claim(to, epoch.number); return _sendGhstBased(claimedAmount, to, isRebase); } function claimByAmount( address to, uint256 amount, bool isRebase ) public override returns (uint256) { if (!unlocks[to] && to != msg.sender) revert ExternalDepositsLocked(); uint256 claimedAmount = IGhostWarmup(warmup).claimByAmount(to, amount, epoch.number); return _sendGhstBased(claimedAmount, to, isRebase); } function breakout( bytes32 receiver, uint256 amount ) public override returns (uint256 claimedAmount) { claimedAmount = IGhostWarmup(warmup).breakout(msg.sender, amount); IGatekeeper(gatekeeper).ghost(receiver, claimedAmount); } function forfeit() external override returns (uint256 deposit) { deposit = IGhostWarmup(warmup).forfeit(msg.sender); IERC20(ftso).safeTransfer(msg.sender, deposit); } function toggleLock() external override { unlocks[msg.sender] = !unlocks[msg.sender]; } function unstake( uint256 amount, address to, bool isTrigger, bool isRebase ) external override returns (uint256) { if (!unlocks[to] && to != msg.sender) revert ExternalDepositsLocked(); uint256 extraAmount = isTrigger ? this.rebase() : 0; if (isRebase) { amount += extraAmount; ISTNK(stnk).safeTransferFrom(msg.sender, address(this), amount); } else { IGHST(ghst).burn(msg.sender, amount); amount = IGHST(ghst).balanceFrom(amount) + extraAmount; } if (amount > IERC20(ftso).balanceOf(address(this))) revert InsufficientBalance(); IERC20(ftso).safeTransfer(to, amount); emit Unstaked(msg.sender, to, amount, isTrigger, isRebase); return amount; } function wrap( address to, uint256 amount ) external override returns (uint256 balance) { if (!unlocks[to] && to != msg.sender) revert ExternalDepositsLocked(); ISTNK(stnk).safeTransferFrom(msg.sender, address(this), amount); balance = IGHST(ghst).balanceTo(amount); IGHST(ghst).mint(to, balance); } function unwrap( address to, uint256 amount ) external override returns (uint256 balance) { if (!unlocks[to] && to != msg.sender) revert ExternalDepositsLocked(); IGHST(ghst).burn(msg.sender, amount); balance = IGHST(ghst).balanceFrom(amount); ISTNK(stnk).safeTransfer(to, balance); } function ghost(bytes32 receiver, uint256 amount) external override { IGHST(ghst).burn(msg.sender, amount); IGatekeeper(gatekeeper).ghost(receiver, amount); } function phantomRefund( address receiver, uint256 amount, uint256 registryIndex, uint256 phantomExpiry ) external override { if (gatekeeper != msg.sender) revert NotGatekeeper(); // forge-lint: disable-next-line(unsafe-typecast) uint48 phantomRelease = uint48(block.timestamp + phantomExpiry); address treasury = ISTNK(stnk).treasury(); ITreasury(treasury).phantomRefund(receiver, phantomRelease, amount, registryIndex); } function recall(address receiver, uint256 amount) external override { if (gatekeeper != msg.sender) revert NotGatekeeper(); IGHST(ghst).mint(receiver, amount); } function recall( uint256 amount, uint256 registryIndex ) external override returns (address reserveToken, uint256 value) { if (gatekeeper != msg.sender) revert NotGatekeeper(); address treasury = ISTNK(stnk).treasury(); uint256 ftsoAmount = IGHST(ghst).balanceFrom(amount); (reserveToken, value) = ITreasury(treasury).buyBack( msg.sender, ftsoAmount, registryIndex ); IFTSO(ftso).burn(ftsoAmount); } function rebase() public override returns (uint256 bounty) { if (epoch.end <= block.timestamp && block.number > _lastRebaseBlock) { ISTNK(stnk).rebase(epoch.distribute, epoch.number); unchecked { epoch.end += epoch.length; ++epoch.number; } if (distributor != address(0)) { IDistributor(distributor).distribute(); if (msg.sender == address(this)) { bounty = IDistributor(distributor).retrieveBounty(); } } uint256 balance = IERC20(ftso).balanceOf(address(this)); uint256 extra = ISTNK(stnk).circulatingSupply() + bounty; _lastRebaseBlock = block.number; epoch.distribute = balance > extra ? balance - extra : 0; } } function setDistributor(address _distributor) external onlyGovernor { distributor = _distributor; emit DistributorSet(_distributor); } function setWarmupPeriod(uint256 _warmupPeriod) external onlyGovernor { // forge-lint: disable-next-line(unsafe-typecast) warmupPeriod = uint48(_warmupPeriod); emit WarmupSet(_warmupPeriod); } function updateGatekeeperAddress(address newGatekeeper) external onlyGovernor { IGatekeeper(newGatekeeper).initialize(gatekeeper); address storageHistory = IGatekeeper(gatekeeper).storageHistory(); IStorageHistory(storageHistory).setOwner(newGatekeeper); gatekeeper = newGatekeeper; emit GatekeeperSet(newGatekeeper); } function index() public view override returns (uint256) { return ISTNK(stnk).index(); } function supplyInWarmup() public view override returns (uint256) { return IGHST(ghst).balanceFrom(IGhostWarmup(warmup).ghstInWarmup()); } function ghostedSupply() public view override returns (uint256 amount) { if (gatekeeper != address(0)) { amount = IGatekeeper(gatekeeper).ghostedSupply(); } } function warmupInfo(address who) external view returns (uint256, uint256, uint48, bool) { (uint256 deposit, uint256 payout, uint48 expiry) = IGhostWarmup(warmup).warmupInfo(who); bool matured = expiry > 0 && epoch.number >= expiry; return (deposit, payout, expiry, matured); } 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, bool isRebase ) internal returns (uint256) { if (isRebase) { ISTNK(stnk).safeTransfer(to, amount); return amount; } else { uint256 balanceTo = IGHST(ghst).balanceTo(amount); IGHST(ghst).mint(to, balanceTo); return balanceTo; } } function _sendGhstBased( uint256 amount, address to, bool isRebase ) internal returns (uint256) { if (isRebase) { uint256 convertedAmount = IGHST(ghst).balanceFrom(amount); ISTNK(stnk).safeTransfer(to, convertedAmount); return convertedAmount; } else { IGHST(ghst).mint(to, amount); return amount; } } }