319 lines
11 KiB
Solidity
319 lines
11 KiB
Solidity
// 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;
|
|
}
|
|
}
|
|
}
|