Skip to content

Commit

Permalink
Merge branch 'liquid-staking' of github.com:mstable/mStable-contracts…
Browse files Browse the repository at this point in the history
… into liquid-staking
  • Loading branch information
alsco77 committed Sep 1, 2021
2 parents 361317e + 494feb0 commit 1061a9d
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 23 deletions.
53 changes: 44 additions & 9 deletions contracts/governance/staking/GamifiedToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ abstract contract GamifiedToken is

/// @notice User balance structs containing all data needed to scale balance
mapping(address => Balance) internal _balances;
/// @notice Most recent price coefficients per user
mapping(address => uint16) internal _userPriceCoeff;
/// @notice Quest Manager
QuestManager public immutable questManager;
/// @notice Has variable price
bool public immutable hasPriceCoeff;

/***************************************
INIT
Expand All @@ -52,9 +56,11 @@ abstract contract GamifiedToken is
constructor(
address _nexus,
address _rewardsToken,
address _questManager
address _questManager,
bool _hasPriceCoeff
) HeadlessStakingRewards(_nexus, _rewardsToken) {
questManager = QuestManager(_questManager);
hasPriceCoeff = _hasPriceCoeff;
}

/**
Expand Down Expand Up @@ -107,7 +113,7 @@ abstract contract GamifiedToken is
override(HeadlessStakingRewards, ILockedERC20)
returns (uint256)
{
return _getBalance(_balances[_account]);
return _getBalance(_account, _balances[_account]);
}

/**
Expand All @@ -121,12 +127,21 @@ abstract contract GamifiedToken is
/**
* @dev Scales the balance of a given user by applying multipliers
*/
function _getBalance(Balance memory _balance) internal pure returns (uint256 balance) {
function _getBalance(address _account, Balance memory _balance)
internal
view
returns (uint256 balance)
{
// e.g. raw = 1000, questMultiplier = 40, timeMultiplier = 30. Cooldown of 60%
// e.g. 1000 * (100 + 40) / 100 = 1400
balance = (_balance.raw * (100 + _balance.questMultiplier)) / 100;
// e.g. 1400 * (100 + 30) / 100 = 1820
balance = (balance * (100 + _balance.timeMultiplier)) / 100;

if (hasPriceCoeff) {
// e.g. 1820 * 16000 / 10000 = 2912
balance = (balance * _userPriceCoeff[_account]) / 10000;
}
}

/**
Expand All @@ -136,6 +151,13 @@ abstract contract GamifiedToken is
return _balances[_account];
}

/**
* @notice Raw staked balance without any multipliers
*/
function userPriceCoeff(address _account) external view returns (uint16) {
return _userPriceCoeff[_account];
}

/***************************************
QUESTS
****************************************/
Expand All @@ -162,7 +184,7 @@ abstract contract GamifiedToken is

// 1. Get current balance & update questMultiplier, only if user has a balance
Balance memory oldBalance = _balances[_account];
uint256 oldScaledBalance = _getBalance(oldBalance);
uint256 oldScaledBalance = _getBalance(_account, oldBalance);
if (oldScaledBalance > 0) {
_applyQuestMultiplier(_account, oldBalance, oldScaledBalance, _newMultiplier);
}
Expand Down Expand Up @@ -199,6 +221,10 @@ abstract contract GamifiedToken is
}
}

function _getPriceCoeff() internal virtual returns (uint16) {
return 10000;
}

/***************************************
BALANCE CHANGES
****************************************/
Expand Down Expand Up @@ -327,7 +353,7 @@ abstract contract GamifiedToken is
// i) For new _account, set up weighted timestamp
if (oldBalance.weightedTimestamp == 0) {
_balances[_account].weightedTimestamp = SafeCastExtended.toUint32(block.timestamp);
_mintScaled(_account, _getBalance(_balances[_account]));
_mintScaled(_account, _getBalance(_account, _balances[_account]));
return;
}
// ii) For previous minters, recalculate time held
Expand Down Expand Up @@ -417,9 +443,12 @@ abstract contract GamifiedToken is
{
// Get the old balance
oldBalance = _balances[_account];
oldScaledBalance = _getBalance(oldBalance);
oldScaledBalance = _getBalance(_account, oldBalance);
// Take the opportunity to check for season finish
_balances[_account].questMultiplier = questManager.checkForSeasonFinish(_account);
if (hasPriceCoeff) {
_userPriceCoeff[_account] = _getPriceCoeff();
}
}

/**
Expand All @@ -431,7 +460,7 @@ abstract contract GamifiedToken is
* @param _oldScaledBalance Previous scaled balance of the user
*/
function _settleScaledBalance(address _account, uint256 _oldScaledBalance) private {
uint256 newScaledBalance = _getBalance(_balances[_account]);
uint256 newScaledBalance = _getBalance(_account, _balances[_account]);
if (newScaledBalance > _oldScaledBalance) {
_mintScaled(_account, newScaledBalance - _oldScaledBalance);
}
Expand Down Expand Up @@ -474,10 +503,16 @@ abstract contract GamifiedToken is
*/
function _claimRewardHook(address _account) internal override {
uint8 newMultiplier = questManager.checkForSeasonFinish(_account);
if (newMultiplier != _balances[_account].questMultiplier) {
bool priceCoeffChanged = hasPriceCoeff
? _getPriceCoeff() != _userPriceCoeff[_account]
: false;
if (newMultiplier != _balances[_account].questMultiplier || priceCoeffChanged) {
// 1. Get current balance & trigger season finish
uint256 oldScaledBalance = _getBalance(_balances[_account]);
uint256 oldScaledBalance = _getBalance(_account, _balances[_account]);
_balances[_account].questMultiplier = newMultiplier;
if (priceCoeffChanged) {
_userPriceCoeff[_account] = _getPriceCoeff();
}
// 3. Update scaled balance
_settleScaledBalance(_account, oldScaledBalance);
}
Expand Down
5 changes: 3 additions & 2 deletions contracts/governance/staking/GamifiedVotingToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ abstract contract GamifiedVotingToken is Initializable, GamifiedToken {
constructor(
address _nexus,
address _rewardsToken,
address _questManager
) GamifiedToken(_nexus, _rewardsToken, _questManager) {}
address _questManager,
bool _hasPriceCoeff
) GamifiedToken(_nexus, _rewardsToken, _questManager, _hasPriceCoeff) {}

function __GamifiedVotingToken_init() internal initializer {}

Expand Down
9 changes: 5 additions & 4 deletions contracts/governance/staking/StakedToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ contract StakedToken is GamifiedVotingToken {
address _questManager,
address _stakedToken,
uint256 _cooldownSeconds,
uint256 _unstakeWindow
) GamifiedVotingToken(_nexus, _rewardsToken, _questManager) {
uint256 _unstakeWindow,
bool _hasPriceCoeff
) GamifiedVotingToken(_nexus, _rewardsToken, _questManager, _hasPriceCoeff) {
STAKED_TOKEN = IERC20(_stakedToken);
COOLDOWN_SECONDS = _cooldownSeconds;
UNSTAKE_WINDOW = _unstakeWindow;
Expand All @@ -89,11 +90,11 @@ contract StakedToken is GamifiedVotingToken {
* @param _symbolArg Token symbol
* @param _rewardsDistributorArg mStable Rewards Distributor
*/
function initialize(
function __StakedToken_init(
string memory _nameArg,
string memory _symbolArg,
address _rewardsDistributorArg
) external initializer {
) public initializer {
__GamifiedToken_init(_nameArg, _symbolArg, _rewardsDistributorArg);
safetyData = SafetyData({ collateralisationRatio: 1e18, slashingPercentage: 0 });
}
Expand Down
114 changes: 109 additions & 5 deletions contracts/governance/staking/StakedTokenBPT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.6;
pragma abicoder v2;

import { StakedToken } from "./StakedToken.sol";
import { SafeCastExtended } from "../../shared/SafeCastExtended.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IBVault, ExitPoolRequest } from "./interfaces/IBVault.sol";
Expand All @@ -27,19 +28,34 @@ contract StakedTokenBPT is StakedToken {
/// @notice Core token that is staked and tracked (e.g. MTA)
address public balRecipient;

/// @notice Keeper
address public keeper;

/// @notice Pending fees in BPT terms
uint256 public pendingBPTFees;

/// @notice Most recent PriceCoefficient
uint16 public priceCoefficient;

/// @notice Time of last priceCoefficient upgrade
uint32 public lastPriceUpdateTime;

event KeeperUpdated(address newKeeper);
event BalClaimed();
event BalRecipientChanged(address newRecipient);
event PriceCoefficientUpdated(uint16 newPriceCoeff);

/***************************************
INIT
****************************************/

/**
* @param _nexus System nexus
* @param _rewardsToken Token that is being distributed as a reward. eg MTA
* @param _stakedToken Core token that is staked and tracked (e.g. MTA)
* @param _cooldownSeconds Seconds a user must wait after she initiates her cooldown before withdrawal is possible
* @param _unstakeWindow Window in which it is possible to withdraw, following the cooldown period
* @param _bal Balancer addresses, [0] = $BAL addr, [1] = designated recipient, [2] = BAL vault
* @param _bal Balancer addresses, [0] = $BAL addr, [1] = BAL vault
* @param _poolId Balancer Pool identifier
*/
constructor(
Expand All @@ -49,7 +65,7 @@ contract StakedTokenBPT is StakedToken {
address _stakedToken,
uint256 _cooldownSeconds,
uint256 _unstakeWindow,
address[3] memory _bal,
address[2] memory _bal,
bytes32 _poolId
)
StakedToken(
Expand All @@ -58,15 +74,40 @@ contract StakedTokenBPT is StakedToken {
_questManager,
_stakedToken,
_cooldownSeconds,
_unstakeWindow
_unstakeWindow,
true
)
{
BAL = IERC20(_bal[0]);
balRecipient = _bal[1];
balancerVault = IBVault(_bal[2]);
balancerVault = IBVault(_bal[1]);
poolId = _poolId;
}

/**
* @param _nameArg Token name
* @param _symbolArg Token symbol
* @param _rewardsDistributorArg mStable Rewards Distributor
*/
function initialize(
string memory _nameArg,
string memory _symbolArg,
address _rewardsDistributorArg,
address[1] memory _bal
) internal initializer {
__StakedToken_init(_nameArg, _symbolArg, _rewardsDistributorArg);
balRecipient = _bal[0];
priceCoefficient = 10000;
}

modifier governorOrKeeper() {
require(_msgSender() == _governor() || _msgSender() == keeper, "Gov or keeper");
_;
}

/***************************************
BAL incentives
****************************************/

/**
* @dev Claims any $BAL tokens present on this address as part of any potential liquidity mining program
*/
Expand All @@ -86,6 +127,10 @@ contract StakedTokenBPT is StakedToken {
emit BalRecipientChanged(_newRecipient);
}

/***************************************
FEES
****************************************/

/**
* @dev Converts fees accrued in BPT into MTA, before depositing to the rewards contract
*/
Expand Down Expand Up @@ -141,4 +186,63 @@ contract StakedTokenBPT is StakedToken {

pendingBPTFees += _additionalReward;
}

/***************************************
PRICE
****************************************/

/**
* @dev Sets the keeper that is responsible for fetching new price coefficients
*/
function setKeeper(address _newKeeper) external onlyGovernor {
keeper = _newKeeper;

emit KeeperUpdated(_newKeeper);
}

/**
* @dev Fetches most recent priceCoeff from the balancer pool.
* PriceCoeff = units of MTA per BPT, scaled to 1:1 = 10000
* Assuming an 80/20 BPT, it is possible to calculate
* PriceCoeff (p) = balanceOfMTA in pool (b) / bpt supply (s) / 0.8
* p = b * 1.25 / s
*/
function fetchPriceCoefficient() external governorOrKeeper {
require(
block.timestamp > lastPriceUpdateTime + 14 days,
"Maximum one update per 14 days allowed"
);

(address[] memory tokens, uint256[] memory balances, ) = balancerVault.getPoolTokens(
poolId
);
require(tokens[0] == address(REWARDS_TOKEN), "MTA in wrong place");

// Calculate units of MTA per BPT
// e.g. 800e18 * 125e16 / 1000e18 = 1e18
// e.g. 1280e18 * 125e16 / 1000e18 = 16e17
uint256 unitsPerToken = (balances[0] * 125e16) / STAKED_TOKEN.totalSupply();
// e.g. 1e18 / 1e14 = 10000
// e.g. 16e17 / 1e14 = 16000
uint16 newPriceCoeff = SafeCastExtended.toUint16(unitsPerToken / 1e14);
uint16 oldPriceCoeff = priceCoefficient;
uint16 diff = newPriceCoeff > oldPriceCoeff
? newPriceCoeff - oldPriceCoeff
: oldPriceCoeff - newPriceCoeff;

require(diff > 500, "Must be > 5% diff");
require(newPriceCoeff > 4000 && newPriceCoeff < 22000, "Out of bounds");

priceCoefficient = newPriceCoeff;
lastPriceUpdateTime = SafeCastExtended.toUint32(block.timestamp);

emit PriceCoefficientUpdated(newPriceCoeff);
}

/**
* @dev Get the current priceCoeff
*/
function _getPriceCoeff() internal view override returns (uint16) {
return priceCoefficient;
}
}
11 changes: 10 additions & 1 deletion contracts/governance/staking/StakedTokenMTA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,19 @@ contract StakedTokenMTA is StakedToken {
_questManager,
_stakedToken,
_cooldownSeconds,
_unstakeWindow
_unstakeWindow,
false
)
{}

function initialize(
string memory _nameArg,
string memory _symbolArg,
address _rewardsDistributorArg
) external initializer {
__StakedToken_init(_nameArg, _symbolArg, _rewardsDistributorArg);
}

/**
* @dev Allows a staker to compound their rewards IF the Staking token and the Rewards token are the same
* for example, with $MTA as both staking token and rewards token. Calls 'claimRewards' on the HeadlessStakingRewards
Expand Down
Loading

0 comments on commit 1061a9d

Please sign in to comment.