ghost-dao-contracts/src/Staking.sol

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