Skip to content

Commit

Permalink
Merge branch 'cooldown-units' into liquid-staking
Browse files Browse the repository at this point in the history
  • Loading branch information
alsco77 committed Aug 20, 2021
2 parents 46470fa + 330e602 commit 46195a6
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 217 deletions.
74 changes: 47 additions & 27 deletions contracts/governance/staking/GamifiedToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ abstract contract GamifiedToken is
/// @notice A whitelisted questMaster who can add quests
address internal _questMaster;

/// @notice Tracks the cooldowns for all users
mapping(address => CooldownData) public stakersCooldowns;

event QuestAdded(
address questMaster,
uint256 id,
Expand Down Expand Up @@ -142,8 +145,8 @@ abstract contract GamifiedToken is
* @dev Simply gets raw balance
* @return raw balance for user
*/
function rawBalanceOf(address _account) public view returns (uint256) {
return _balances[_account].raw;
function rawBalanceOf(address _account) public view returns (uint256, uint256) {
return (_balances[_account].raw, stakersCooldowns[_account].units);
}

/**
Expand All @@ -157,11 +160,6 @@ abstract contract GamifiedToken is
100;
// e.g. 1400 * (100 + 30) / 100 = 1820
balance = (balance * (100 + _balance.timeMultiplier)) / 100;
// If the user is in cooldown, their balance is temporarily slashed depending on % of withdrawal
if (_balance.cooldownMultiplier > 0) {
// e.g. 1820 * (100 - 60) / 100 = 728
balance = (balance * (100 - _balance.cooldownMultiplier)) / 100;
}
}

/**
Expand Down Expand Up @@ -323,24 +321,32 @@ abstract contract GamifiedToken is
* @dev Entering a cooldown period means a user wishes to withdraw. With this in mind, their balance
* should be reduced until they have shown more commitment to the system
* @param _account Address of user that should be cooled
* @param _percentage Percentage of total stake to cooldown for, where 100% = 1e18
* @param _units Units to cooldown for
*/
function _enterCooldownPeriod(address _account, uint256 _percentage)
function _enterCooldownPeriod(address _account, uint256 _units)
internal
updateReward(_account)
{
require(_account != address(0), "Invalid address");
require(_percentage > 0 && _percentage <= 1e18, "Must choose between 0 and 100%");

// 1. Get current balance
(Balance memory oldBalance, uint256 oldScaledBalance) = _prepareOldBalance(_account);
CooldownData memory cooldownData = stakersCooldowns[_account];
uint128 totalUnits = _balances[_account].raw + cooldownData.units;
require(_units > 0 && _units <= totalUnits, "Must choose between 0 and 100%");

// 2. Set weighted timestamp and enter cooldown
_balances[_account].timeMultiplier = _timeMultiplier(oldBalance.weightedTimestamp);
// e.g. 1e18 / 1e16 = 100, 2e16 / 1e16 = 2, 1e15/1e16 = 0
_balances[_account].cooldownMultiplier = SafeCast.toUint16(_percentage / 1e16);
_balances[_account].raw = totalUnits - SafeCast.toUint128(_units);

// 3. Update scaled balance
// 3. Set cooldown data
stakersCooldowns[_account] = CooldownData({
timestamp: SafeCast.toUint128(block.timestamp),
units: SafeCast.toUint128(_units)
});

// 4. Update scaled balance
_settleScaledBalance(_account, oldScaledBalance);
}

Expand All @@ -356,9 +362,12 @@ abstract contract GamifiedToken is

// 2. Set weighted timestamp and enter cooldown
_balances[_account].timeMultiplier = _timeMultiplier(oldBalance.weightedTimestamp);
_balances[_account].cooldownMultiplier = 0;
_balances[_account].raw += stakersCooldowns[_account].units;

// 3. Update scaled balance
// 3. Set cooldown data
stakersCooldowns[_account] = CooldownData(0, 0);

// 4. Update scaled balance
_settleScaledBalance(_account, oldScaledBalance);
}

Expand Down Expand Up @@ -419,21 +428,26 @@ abstract contract GamifiedToken is
* Importantly, when a user stakes more, their weightedTimestamp is reduced proportionate to their stake.
* @param _account Address of user to credit
* @param _rawAmount Raw amount of tokens staked
* @param _newPercentage Set the users cooldown slash
* @param _exitCooldown Should we end any cooldown?
*/
function _mintRaw(
address _account,
uint256 _rawAmount,
uint256 _newPercentage
bool _exitCooldown
) internal virtual updateReward(_account) {
require(_account != address(0), "ERC20: mint to the zero address");

// 1. Get and update current balance
(Balance memory oldBalance, uint256 oldScaledBalance) = _prepareOldBalance(_account);
CooldownData memory cooldownData = stakersCooldowns[_account];
uint256 totalRaw = oldBalance.raw + cooldownData.units;
_balances[_account].raw = oldBalance.raw + SafeCast.toUint128(_rawAmount);

// 2. Exit cooldown if necessary
_balances[_account].cooldownMultiplier = SafeCast.toUint16(_newPercentage / 1e16);
if (_exitCooldown) {
_balances[_account].raw += cooldownData.units;
stakersCooldowns[_account] = CooldownData(0, 0);
}

// 3. Set weighted timestamp
// i) For new _account, set up weighted timestamp
Expand All @@ -445,8 +459,8 @@ abstract contract GamifiedToken is
// ii) For previous minters, recalculate time held
// Calc new weighted timestamp
uint256 oldWeighredSecondsHeld = (block.timestamp - oldBalance.weightedTimestamp) *
oldBalance.raw;
uint256 newSecondsHeld = oldWeighredSecondsHeld / (oldBalance.raw + (_rawAmount / 2));
totalRaw;
uint256 newSecondsHeld = oldWeighredSecondsHeld / (totalRaw + (_rawAmount / 2));
uint32 newWeightedTs = SafeCast.toUint32(block.timestamp - newSecondsHeld);
_balances[_account].weightedTimestamp = newWeightedTs;

Expand All @@ -461,32 +475,38 @@ abstract contract GamifiedToken is
* @dev Called to burn a given amount of raw tokens.
* @param _account Address of user
* @param _rawAmount Raw amount of tokens to remove
* @param _cooldownPercentage Set new cooldown percentage
* @param _exitCooldown Exit the cooldown?
*/
function _burnRaw(
address _account,
uint256 _rawAmount,
uint128 _cooldownPercentage
bool _exitCooldown
) internal virtual updateReward(_account) {
require(_account != address(0), "ERC20: burn from zero address");

// 1. Get and update current balance
(Balance memory oldBalance, uint256 oldScaledBalance) = _prepareOldBalance(_account);
require(oldBalance.raw >= _rawAmount, "ERC20: burn amount > balance");
CooldownData memory cooldownData = stakersCooldowns[_msgSender()];
uint256 totalRaw = oldBalance.raw + cooldownData.units;
// 1.1 Update
require(cooldownData.units >= _rawAmount, "ERC20: burn amount > balance");
unchecked {
_balances[_account].raw = oldBalance.raw - SafeCast.toUint128(_rawAmount);
stakersCooldowns[_account].units -= SafeCast.toUint128(_rawAmount);
}

// 2. Change the cooldown percentage based on size of recent withdrawal
_balances[_account].cooldownMultiplier = SafeCast.toUint16(_cooldownPercentage / 1e16);
// 2. If we are exiting cooldown, reset the balance
if (_exitCooldown) {
_balances[_account].raw += stakersCooldowns[_account].units;
stakersCooldowns[_account] = CooldownData(0, 0);
}

// 3. Set back scaled time
// e.g. stake 10 for 100 seconds, withdraw 5.
// secondsHeld = (100 - 0) * (10 - 1.25) = 875
uint256 secondsHeld = (block.timestamp - oldBalance.weightedTimestamp) *
(oldBalance.raw - (_rawAmount / 4));
(totalRaw - (_rawAmount / 4));
// newWeightedTs = 875 / 100 = 87.5
uint256 newSecondsHeld = secondsHeld / oldBalance.raw;
uint256 newSecondsHeld = secondsHeld / totalRaw;
uint32 newWeightedTs = SafeCast.toUint32(block.timestamp - newSecondsHeld);
_balances[_account].weightedTimestamp = newWeightedTs;

Expand Down
9 changes: 7 additions & 2 deletions contracts/governance/staking/GamifiedTokenStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ struct Balance {
uint16 seasonMultiplier;
/// multiplier awarded for staking for a long time
uint16 timeMultiplier;
/// shows if a user has entered their cooldown ready for a withdrawal. Can be used to slash voting balance
uint16 cooldownMultiplier;
}

/// @notice Quests can either give permanent rewards or only for the season
Expand All @@ -41,3 +39,10 @@ struct Quest {
/// Expiry date in seconds for the quest
uint32 expiry;
}

struct CooldownData {
/// Time at which the relative cooldown began
uint128 timestamp;
/// Units up for cooldown
uint128 units;
}
77 changes: 20 additions & 57 deletions contracts/governance/staking/StakedToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
uint256 public immutable UNSTAKE_WINDOW;
/// @notice A week
uint256 private constant ONE_WEEK = 7 days;
/// @notice cooldown percentage scale where 100% = 1e18. 1% = 1e16
uint256 public constant COOLDOWN_PERCENTAGE_SCALE = 1e18;

struct SafetyData {
/// Percentage of collateralisation where 100% = 1e18
Expand All @@ -51,15 +49,6 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
/// @notice Data relating to the re-collateralisation safety module
SafetyData public safetyData;

struct CooldownData {
/// Time at which the relative cooldown began
uint128 timestamp;
/// Percentage of a users funds up for cooldown where 100% = 1e18
uint128 percentage;
}

/// @notice Tracks the cooldowns for all users
mapping(address => CooldownData) public stakersCooldowns;
/// @notice Whitelisted smart contract integrations
mapping(address => bool) public whitelistedWrappers;

Expand Down Expand Up @@ -222,22 +211,12 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
// then reset the timestamp to 0
bool exitCooldown = _exitCooldown ||
block.timestamp > (oldCooldown.timestamp + COOLDOWN_SECONDS + UNSTAKE_WINDOW);
uint256 newPercentage = 0;
if (exitCooldown) {
stakersCooldowns[_msgSender()] = CooldownData(0, 0);
emit CooldownExited(_msgSender());
} else {
// Set new percentage so amount being cooled is the same as before the this stake.
// new percentage = old percentage * old staked balance / (old staked balance + new staked amount)
uint256 stakedAmountOld = uint256(_balances[_msgSender()].raw);
newPercentage =
(oldCooldown.percentage * stakedAmountOld) /
(stakedAmountOld + _amount);
stakersCooldowns[_msgSender()].percentage = SafeCast.toUint128(newPercentage);
}

// 3. Settle the stake by depositing the STAKED_TOKEN and minting voting power
_mintRaw(_msgSender(), _amount, newPercentage);
_mintRaw(_msgSender(), _amount, exitCooldown);

emit Staked(_msgSender(), _amount, _delegatee);
}
Expand Down Expand Up @@ -278,7 +257,7 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
// Is the contract post-recollateralisation?
if (safetyData.collateralisationRatio != 1e18) {
// 1. If recollateralisation has occured, the contract is finished and we can skip all checks
_burnRaw(_msgSender(), _amount, 0);
_burnRaw(_msgSender(), _amount, false);
// 2. Return a proportionate amount of tokens, based on the collateralisation ratio
STAKED_TOKEN.safeTransfer(
_recipient,
Expand Down Expand Up @@ -311,30 +290,15 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
uint256 userWithdrawal = (totalWithdraw * 1e18) / (1e18 + feeRate);

// Check for percentage withdrawal
uint256 maxWithdrawal = (uint256(balance.raw) * cooldown.percentage) /
COOLDOWN_PERCENTAGE_SCALE;
uint256 maxWithdrawal = cooldown.units;
require(totalWithdraw <= maxWithdrawal, "Exceeds max withdrawal");

// 4. Exit cooldown if the user has specified, or if they have withdrawn everything
// Otherwise, update the percentage remaining proportionately
bool exitCooldown = _exitCooldown || totalWithdraw == maxWithdrawal;
uint128 cooldownPercentage = 0;
if (exitCooldown) {
stakersCooldowns[_msgSender()] = CooldownData(0, 0);
} else {
// e.g. stake 1000 and have 50% cooldown percentage. Withdraw 400 uses 40% of total
// (500e18-400e18) * 1e18 / (1000e18 - 400e18) = 100e18 / 600e18 = 16e16 (16% of new total allowance)
cooldownPercentage = SafeCast.toUint128(
((maxWithdrawal - totalWithdraw) * COOLDOWN_PERCENTAGE_SCALE) /
(uint256(balance.raw) - totalWithdraw)
);
console.log("max, total, user");
console.log(maxWithdrawal, totalWithdraw, userWithdrawal);
stakersCooldowns[_msgSender()].percentage = cooldownPercentage;
}

// 5. Settle the withdrawal by burning the voting tokens
_burnRaw(_msgSender(), totalWithdraw, cooldownPercentage);
_burnRaw(_msgSender(), totalWithdraw, exitCooldown);
// Log any redemption fee to the rewards contract
_notifyAdditionalReward(totalWithdraw - userWithdrawal);
// Finally transfer tokens back to recipient
Expand All @@ -348,10 +312,10 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
* @dev Enters a cooldown period, after which (and before the unstake window elapses) a user will be able
* to withdraw part or all of their staked tokens. Note, during this period, a users voting power is significantly reduced.
* If a user already has a cooldown period, then it will reset to the current block timestamp, so use wisely.
* @param _percentage Percentage of total stake to cooldown for, where 100% = 1e18
* @param _units Units of stake to cooldown for
**/
function startCooldown(uint256 _percentage) external override {
_startCooldown(_percentage);
function startCooldown(uint256 _units) external override {
_startCooldown(_units);
}

/**
Expand All @@ -362,7 +326,6 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
function endCooldown() external {
require(stakersCooldowns[_msgSender()].timestamp != 0, "No cooldown");

stakersCooldowns[_msgSender()] = CooldownData(0, 0);
_exitCooldownPeriod(_msgSender());

emit CooldownExited(_msgSender());
Expand All @@ -372,19 +335,14 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
* @dev Enters a cooldown period, after which (and before the unstake window elapses) a user will be able
* to withdraw part or all of their staked tokens. Note, during this period, a users voting power is significantly reduced.
* If a user already has a cooldown period, then it will reset to the current block timestamp, so use wisely.
* @param _percentage Percentage of total stake to cooldown for, where 100% = 1e18
* @param _units Units of stake to cooldown for
**/
function _startCooldown(uint256 _percentage) internal {
function _startCooldown(uint256 _units) internal {
require(balanceOf(_msgSender()) != 0, "INVALID_BALANCE_ON_COOLDOWN");
require(_percentage > 0 && _percentage <= COOLDOWN_PERCENTAGE_SCALE, "Invalid percentage");

stakersCooldowns[_msgSender()] = CooldownData({
timestamp: SafeCast.toUint128(block.timestamp),
percentage: SafeCast.toUint128(_percentage)
});
_enterCooldownPeriod(_msgSender(), _percentage);
_enterCooldownPeriod(_msgSender(), _units);

emit Cooldown(_msgSender(), _percentage);
emit Cooldown(_msgSender(), _units);
}

/***************************************
Expand Down Expand Up @@ -491,10 +449,15 @@ contract StakedToken is IStakedToken, GamifiedVotingToken {
**/
function exit() external virtual {
// Since there is no immediate exit here, this can be called twice
if (stakersCooldowns[_msgSender()].timestamp == 0) {
_startCooldown(1e18);
} else {
_withdraw(_balances[_msgSender()].raw, _msgSender(), true, false);
// If there is no cooldown, or the cooldown has passed the unstake window, enter cooldown
uint128 ts = stakersCooldowns[_msgSender()].timestamp;
if (ts == 0 || block.timestamp > ts + COOLDOWN_SECONDS + UNSTAKE_WINDOW) {
(uint256 raw, uint256 cooldownUnits) = rawBalanceOf(_msgSender());
_startCooldown(raw + cooldownUnits);
}
// Else withdraw all available
else {
_withdraw(stakersCooldowns[_msgSender()].units, _msgSender(), true, false);
}
}

Expand Down
Loading

0 comments on commit 46195a6

Please sign in to comment.