mirror of https://github.com/logos-co/staking.git
286 lines
10 KiB
Solidity
286 lines
10 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
|
|
pragma solidity ^0.8.18;
|
|
|
|
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
|
|
import { StakeVault } from "./StakeVault.sol";
|
|
|
|
contract StakeManager is Ownable {
|
|
error StakeManager__SenderIsNotVault();
|
|
error StakeManager__FundsLocked();
|
|
error StakeManager__DecreasingLockTime();
|
|
error StakeManager__NoPendingMigration();
|
|
error StakeManager__PendingMigration();
|
|
error StakeManager__SenderIsNotPreviousStakeManager();
|
|
error StakeManager__InvalidLimitEpoch();
|
|
error StakeManager__InvalidLockupPeriod();
|
|
|
|
struct Account {
|
|
uint256 lockUntil;
|
|
uint256 balance;
|
|
uint256 multiplier;
|
|
uint256 lastMint;
|
|
uint256 epoch;
|
|
address rewardAddress;
|
|
}
|
|
|
|
struct Epoch {
|
|
uint256 startTime;
|
|
uint256 epochReward;
|
|
uint256 totalSupply;
|
|
}
|
|
|
|
uint256 public constant EPOCH_SIZE = 1 weeks;
|
|
uint256 public constant YEAR = 365 days;
|
|
uint256 public constant MIN_LOCKUP_PERIOD = 12 weeks; // 3 months
|
|
uint256 public constant MAX_LOCKUP_PERIOD = 4 * YEAR; // 4 years
|
|
uint256 public constant MP_APY = 1;
|
|
uint256 public constant MAX_BOOST = 4;
|
|
|
|
mapping(address => Account) accounts;
|
|
mapping(uint256 => Epoch) epochs;
|
|
mapping(bytes32 => bool) isVault;
|
|
|
|
uint256 public currentEpoch;
|
|
uint256 public pendingReward;
|
|
uint256 public multiplierSupply;
|
|
uint256 public stakeSupply;
|
|
StakeManager public migration;
|
|
StakeManager public immutable oldManager;
|
|
ERC20 public immutable stakedToken;
|
|
|
|
modifier onlyVault() {
|
|
if (!isVault[msg.sender.codehash]) {
|
|
revert StakeManager__SenderIsNotVault();
|
|
}
|
|
_;
|
|
}
|
|
|
|
constructor(address _stakedToken, address _oldManager) {
|
|
epochs[0].startTime = block.timestamp;
|
|
oldManager = StakeManager(_oldManager);
|
|
stakedToken = ERC20(_stakedToken);
|
|
}
|
|
|
|
/**
|
|
* Increases balance of msg.sender;
|
|
* @param _amount Amount of balance to be decreased.
|
|
* @param _time Seconds from block.timestamp to lock balance.
|
|
*
|
|
* @dev Reverts when `_time` is not in range of [MIN_LOCKUP_PERIOD, MAX_LOCKUP_PERIOD]
|
|
*/
|
|
function stake(uint256 _amount, uint256 _time) external onlyVault {
|
|
if (_time > 0 && (_time < MIN_LOCKUP_PERIOD || _time > MAX_LOCKUP_PERIOD)) {
|
|
revert StakeManager__InvalidLockupPeriod();
|
|
}
|
|
Account storage account = accounts[msg.sender];
|
|
processAccount(account, currentEpoch);
|
|
account.balance += _amount;
|
|
account.rewardAddress = StakeVault(msg.sender).owner();
|
|
mintIntialMultiplier(account, _time, _amount, 1);
|
|
stakeSupply += _amount;
|
|
}
|
|
|
|
/**
|
|
* Decreases balance of msg.sender;
|
|
* @param _amount Amount of balance to be decreased
|
|
*/
|
|
function unstake(uint256 _amount) external onlyVault {
|
|
Account storage account = accounts[msg.sender];
|
|
if (account.lockUntil > block.timestamp) {
|
|
revert StakeManager__FundsLocked();
|
|
}
|
|
processAccount(account, currentEpoch);
|
|
uint256 reducedMultiplier = (_amount * account.multiplier) / account.balance;
|
|
account.multiplier -= reducedMultiplier;
|
|
account.balance -= _amount;
|
|
multiplierSupply -= reducedMultiplier;
|
|
stakeSupply -= _amount;
|
|
}
|
|
|
|
/**
|
|
* @notice Locks entire balance for more amount of time.
|
|
* @param _time amount of time to lock from now.
|
|
*
|
|
* @dev Reverts when `_time` is bigger than `MAX_LOCKUP_PERIOD`
|
|
* @dev Reverts when `_time + block.timestamp` is smaller than current lock time.
|
|
*/
|
|
function lock(uint256 _time) external onlyVault {
|
|
if (_time > MAX_LOCKUP_PERIOD) {
|
|
revert StakeManager__InvalidLockupPeriod();
|
|
}
|
|
Account storage account = accounts[msg.sender];
|
|
processAccount(account, currentEpoch);
|
|
if (block.timestamp + _time < account.lockUntil) {
|
|
revert StakeManager__DecreasingLockTime();
|
|
}
|
|
mintIntialMultiplier(account, _time, account.balance, 0);
|
|
}
|
|
|
|
/**
|
|
* @notice leave without processing account
|
|
*/
|
|
function leave() external onlyVault {
|
|
if (address(migration) == address(0)) {
|
|
revert StakeManager__NoPendingMigration();
|
|
}
|
|
Account memory account = accounts[msg.sender];
|
|
delete accounts[msg.sender];
|
|
multiplierSupply -= account.multiplier;
|
|
stakeSupply -= account.balance;
|
|
}
|
|
|
|
/**
|
|
* @notice Release rewards for current epoch and increase epoch.
|
|
*/
|
|
function executeEpoch() external {
|
|
processEpoch();
|
|
}
|
|
|
|
/**
|
|
* @notice Execute rewards for account until limit has reached
|
|
* @param _vault Referred account
|
|
* @param _limitEpoch Until what epoch it should be executed
|
|
*/
|
|
function executeAccount(address _vault, uint256 _limitEpoch) external {
|
|
processAccount(accounts[_vault], _limitEpoch);
|
|
}
|
|
|
|
/**
|
|
* @notice Enables a contract class to interact with staking functions
|
|
* @param _codehash bytecode hash of contract
|
|
*/
|
|
function setVault(bytes32 _codehash) external onlyOwner {
|
|
isVault[_codehash] = true;
|
|
}
|
|
/**
|
|
* @notice Migrate account to new manager.
|
|
*/
|
|
|
|
function migrate() external onlyVault returns (StakeManager newManager) {
|
|
if (address(migration) == address(0)) {
|
|
revert StakeManager__NoPendingMigration();
|
|
}
|
|
Account storage account = accounts[msg.sender];
|
|
stakedToken.approve(address(migration), account.balance);
|
|
migration.migrate(msg.sender, account);
|
|
delete accounts[msg.sender];
|
|
return migration;
|
|
}
|
|
|
|
/**
|
|
* @dev Only callable from old manager.
|
|
* @notice Migrate account from old manager
|
|
* @param _vault Account address
|
|
* @param _account Account data
|
|
*/
|
|
function migrate(address _vault, Account memory _account) external {
|
|
if (msg.sender != address(oldManager)) {
|
|
revert StakeManager__SenderIsNotPreviousStakeManager();
|
|
}
|
|
stakedToken.transferFrom(address(oldManager), address(this), _account.balance);
|
|
accounts[_vault] = _account;
|
|
}
|
|
|
|
function calcMaxMultiplierIncrease(
|
|
uint256 _increasedMultiplier,
|
|
uint256 _currentMp,
|
|
uint256 _lockUntil,
|
|
uint256 _stake
|
|
)
|
|
private
|
|
view
|
|
returns (uint256 _maxToIncrease)
|
|
{
|
|
uint256 newMp = _increasedMultiplier + _currentMp;
|
|
if (block.timestamp > _lockUntil) {
|
|
//not locked, limit to max_boost
|
|
return newMp > _stake * MAX_BOOST ? _stake * MAX_BOOST - _currentMp : _increasedMultiplier;
|
|
} else {
|
|
// locked, ignore cap
|
|
return _increasedMultiplier;
|
|
}
|
|
}
|
|
|
|
function processEpoch() private {
|
|
if (block.timestamp >= epochEnd()) {
|
|
//finalize current epoch
|
|
epochs[currentEpoch].epochReward = epochReward();
|
|
epochs[currentEpoch].totalSupply = totalSupply();
|
|
pendingReward += epochs[currentEpoch].epochReward;
|
|
//create new epoch
|
|
currentEpoch++;
|
|
epochs[currentEpoch].startTime = block.timestamp;
|
|
}
|
|
}
|
|
|
|
function processAccount(Account storage account, uint256 _limitEpoch) private {
|
|
processEpoch();
|
|
if (address(migration) != address(0)) {
|
|
revert StakeManager__PendingMigration();
|
|
}
|
|
if (_limitEpoch > currentEpoch) {
|
|
revert StakeManager__InvalidLimitEpoch();
|
|
}
|
|
uint256 userReward;
|
|
uint256 userEpoch = account.epoch;
|
|
for (Epoch memory iEpoch = epochs[userEpoch]; userEpoch < _limitEpoch; userEpoch++) {
|
|
//mint multipliers to that epoch
|
|
mintMultiplier(account, iEpoch.startTime + EPOCH_SIZE);
|
|
uint256 userSupply = account.balance + account.multiplier;
|
|
uint256 userShare = userSupply / iEpoch.totalSupply; //TODO: might lose precision, multiply by 100 and
|
|
// divide back later?
|
|
userReward += userShare * iEpoch.epochReward;
|
|
}
|
|
account.epoch = userEpoch;
|
|
if (userReward > 0) {
|
|
pendingReward -= userReward;
|
|
stakedToken.transfer(account.rewardAddress, userReward);
|
|
}
|
|
mintMultiplier(account, block.timestamp);
|
|
}
|
|
|
|
function mintMultiplier(Account storage account, uint256 processTime) private {
|
|
uint256 deltaTime = processTime - account.lastMint;
|
|
account.lastMint = processTime;
|
|
uint256 increasedMultiplier = calcMaxMultiplierIncrease(
|
|
account.balance * (MP_APY / YEAR * deltaTime), account.multiplier, account.lockUntil, account.balance
|
|
);
|
|
account.multiplier += increasedMultiplier;
|
|
multiplierSupply += increasedMultiplier;
|
|
}
|
|
|
|
function mintIntialMultiplier(
|
|
Account storage account,
|
|
uint256 lockTime,
|
|
uint256 amount,
|
|
uint256 initMint
|
|
)
|
|
private
|
|
{
|
|
//if balance still locked, multipliers must be minted from difference of time.
|
|
uint256 dT = account.lockUntil > block.timestamp ? block.timestamp + lockTime - account.lockUntil : lockTime;
|
|
account.lockUntil = block.timestamp + lockTime;
|
|
uint256 increasedMultiplier = amount * ((dT / YEAR) + initMint);
|
|
account.lastMint = block.timestamp;
|
|
increasedMultiplier = account.multiplier + increasedMultiplier > (account.balance * (MAX_BOOST + (dT / YEAR)))
|
|
? account.balance * (MAX_BOOST + (dT / YEAR)) - account.multiplier
|
|
: increasedMultiplier; // checks if MPs are within (lock_time_in_years+MAX_BOOST)*stake
|
|
multiplierSupply += increasedMultiplier;
|
|
account.multiplier += increasedMultiplier;
|
|
}
|
|
|
|
function totalSupply() public view returns (uint256) {
|
|
return multiplierSupply + stakeSupply;
|
|
}
|
|
|
|
function epochReward() public view returns (uint256) {
|
|
return stakedToken.balanceOf(address(this)) - pendingReward;
|
|
}
|
|
|
|
function epochEnd() public view returns (uint256) {
|
|
return epochs[currentEpoch].startTime + EPOCH_SIZE;
|
|
}
|
|
}
|