190 lines
5.7 KiB
Solidity
190 lines
5.7 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import "@openzeppelin-contracts/token/ERC20/IERC20.sol";
|
|
|
|
import "./types/GhostAccessControlled.sol";
|
|
|
|
import "./interfaces/ISTNK.sol";
|
|
import "./interfaces/IGHST.sol";
|
|
import "./interfaces/IDistributor.sol";
|
|
import "./interfaces/IStaking.sol";
|
|
|
|
contract GhostStaking is IStaking, GhostAccessControlled {
|
|
using SafeERC20 for IERC20;
|
|
using SafeERC20 for ISTNK;
|
|
using SafeERC20 for IGHST;
|
|
|
|
address public immutable ftso;
|
|
address public immutable stnk;
|
|
address public immutable ghst;
|
|
|
|
uint48 public warmupPeriod;
|
|
|
|
Epoch public epoch;
|
|
address public distributor;
|
|
uint256 public sharesInWarmup;
|
|
|
|
mapping(address => Claim) public warmupInfo;
|
|
|
|
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
|
|
});
|
|
}
|
|
|
|
function stake(
|
|
uint256 amount,
|
|
address to,
|
|
bool isRebase,
|
|
bool isClaim
|
|
) external override returns (uint256 returnAmount) {
|
|
returnAmount = amount + rebase();
|
|
IERC20(ftso).safeTransferFrom(msg.sender, address(this), amount);
|
|
if (isClaim && warmupPeriod == 0) {
|
|
returnAmount = _send(returnAmount, to, isRebase);
|
|
} else {
|
|
Claim storage info = warmupInfo[to];
|
|
if (info.lock && to != msg.sender) revert ExternalDepositsLocked();
|
|
|
|
info.deposit += returnAmount;
|
|
info.shares += ISTNK(stnk).sharesForBalance(returnAmount);
|
|
info.expiry = epoch.number + warmupPeriod;
|
|
|
|
sharesInWarmup += ISTNK(stnk).sharesForBalance(returnAmount);
|
|
}
|
|
}
|
|
|
|
function claim(address to, bool isRebase) public override returns (uint256 claimedAmount) {
|
|
Claim memory info = warmupInfo[to];
|
|
if (info.lock && to != msg.sender) revert ExternalDepositsLocked();
|
|
|
|
if (epoch.number >= info.expiry && info.expiry > 0) {
|
|
delete warmupInfo[to];
|
|
sharesInWarmup -= info.shares;
|
|
claimedAmount = _send(ISTNK(stnk).balanceForShares(info.shares), to, isRebase);
|
|
}
|
|
}
|
|
|
|
function forfeit() external override returns (uint256) {
|
|
Claim memory info = warmupInfo[msg.sender];
|
|
delete warmupInfo[msg.sender];
|
|
|
|
sharesInWarmup -= info.shares;
|
|
|
|
IERC20(ftso).safeTransfer(msg.sender, info.deposit);
|
|
return info.deposit;
|
|
}
|
|
|
|
function toggleLock() external override {
|
|
warmupInfo[msg.sender].lock = !warmupInfo[msg.sender].lock;
|
|
}
|
|
|
|
function unstake(
|
|
uint256 amount,
|
|
address to,
|
|
bool isTrigger,
|
|
bool isRebase
|
|
) external override returns (uint256) {
|
|
amount += isTrigger ? rebase() : 0;
|
|
if (isRebase) {
|
|
ISTNK(stnk).safeTransferFrom(msg.sender, address(this), amount);
|
|
} else {
|
|
IGHST(ghst).burn(msg.sender, amount);
|
|
amount = IGHST(ghst).balanceFrom(amount);
|
|
}
|
|
|
|
if (amount > IERC20(ftso).balanceOf(address(this))) revert InsufficientBalance();
|
|
IERC20(ftso).safeTransfer(to, amount);
|
|
return amount;
|
|
}
|
|
|
|
function wrap(
|
|
address to,
|
|
uint256 amount
|
|
) external override returns (uint256 balance) {
|
|
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) {
|
|
IGHST(ghst).burn(msg.sender, amount);
|
|
balance = IGHST(ghst).balanceFrom(amount);
|
|
ISTNK(stnk).safeTransfer(to, balance);
|
|
}
|
|
|
|
function rebase() public override returns (uint256 bounty) {
|
|
if (epoch.end <= block.timestamp) {
|
|
ISTNK(stnk).rebase(epoch.distribute, epoch.number);
|
|
|
|
unchecked {
|
|
epoch.end += epoch.length;
|
|
++epoch.number;
|
|
}
|
|
|
|
if (distributor != address(0)) {
|
|
IDistributor(distributor).distribute();
|
|
bounty = IDistributor(distributor).retrieveBounty();
|
|
}
|
|
|
|
uint256 balance = IERC20(ftso).balanceOf(address(this));
|
|
uint256 extra = ISTNK(stnk).circulatingSupply() + bounty;
|
|
|
|
epoch.distribute = balance > extra ? balance - extra : 0;
|
|
}
|
|
}
|
|
|
|
function setDistributor(address _distributor) external onlyGovernor {
|
|
distributor = _distributor;
|
|
emit DistributorSet(_distributor);
|
|
}
|
|
|
|
function setWarmupPeriod(uint256 _warmupPeriod) external onlyGovernor {
|
|
warmupPeriod = uint48(_warmupPeriod);
|
|
emit WarmupSet(_warmupPeriod);
|
|
}
|
|
|
|
function index() public view override returns (uint256) {
|
|
return ISTNK(stnk).index();
|
|
}
|
|
|
|
function supplyInWarmup() public view override returns (uint256) {
|
|
return ISTNK(stnk).balanceForShares(sharesInWarmup);
|
|
}
|
|
|
|
function _send(
|
|
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;
|
|
}
|
|
}
|
|
}
|