diff --git a/contracts/MultiplierPointMath.sol b/contracts/MultiplierPointMath.sol index 0907036..4de8a07 100644 --- a/contracts/MultiplierPointMath.sol +++ b/contracts/MultiplierPointMath.sol @@ -1,51 +1,135 @@ // SPDX-License-Identifier: MIT-1.0 -pragma solidity ^0.8.18; +pragma solidity ^0.8.26; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; abstract contract MultiplierPointMath { - uint256 public constant YEAR = 365 days; - uint256 public constant MP_APY = 1; + /// @notice One (mean) tropical year, in seconds. + uint256 public constant YEAR = 365 days + 5 hours + 48 minutes + 45 seconds; + /// @notice Multiplier points annual percentage yield. + uint256 public constant MP_APY = 100; + /// @notice Accrued multiplier points maximum multiplier. uint256 public constant MAX_MULTIPLIER = 4; + /// @notice The accrue rate period of time over which multiplier points are calculated. + uint256 public constant ACCURE_RATE = 1 weeks; + /// @notice Minimal value to generate 1 multiplier point in the accrue rate period (rounded up). + uint256 public constant MIN_BALANCE = (((YEAR * 100) - 1) / (MP_APY * ACCURE_RATE)) + 1; + /// @notice Multiplier points absolute maximum multiplier + uint256 public constant MAX_MULTIPLIER_ABSOLUTE = 1 + (2 * (MAX_MULTIPLIER * MP_APY) / 100); + /// @notice Maximum lockup period + uint256 public constant MAX_LOCKUP_PERIOD = MAX_MULTIPLIER * YEAR; /** - * @notice Calculates multiplier points accurred for given `_amount` and `_seconds` time passed - * @param _amount quantity of tokens - * @param _seconds time in seconds + * @notice Calculates the accrued multiplier points (MPs) over a time period Δt, based on the account balance + * @param _balance Represents the current account balance + * @param _deltaTime The time difference or the duration over which the multiplier points are accrued, expressed in + * seconds * @return _accuredMP points accured for given `_amount` and `_seconds` + * 51584438 + * 10000000 */ - function _calculateAccuredMP(uint256 _amount, uint256 _seconds) internal pure returns (uint256 _accuredMP) { - return Math.mulDiv(_amount, _seconds, YEAR) * MP_APY; + function _calculateAccuredMP(uint256 _balance, uint256 _deltaTime) public pure returns (uint256 _accuredMP) { + return Math.mulDiv(_balance, _deltaTime * MP_APY, YEAR * 100); } /** - * @notice Calculates bonus multiplier points for given `_amount` and `_lockedSeconds` + * @notice Calculates the bonus multiplier points (MPs) earned when a balance Δa is locked for a specified duration + * t_lock. + * It is equivalent to the accrued multiplier points function but specifically applied in the context of a locked + * balance. * @param _amount quantity of tokens * @param _lockedSeconds time in seconds locked * @return _bonusMP bonus multiplier points for given `_amount` and `_lockedSeconds` */ - function _calculateBonusMP(uint256 _amount, uint256 _lockedSeconds) internal pure returns (uint256 _bonusMP) { - _bonusMP = _amount; - if (_lockedSeconds > 0) { - _bonusMP += _calculateAccuredMP(_amount, _lockedSeconds); - } + function _calculateBonusMP(uint256 _amount, uint256 _lockedSeconds) public pure returns (uint256 _bonusMP) { + return _calculateAccuredMP(_amount, _lockedSeconds); } /** - * @notice Calculates minimum stake to genarate 1 multiplier points for given `_seconds` - * @param _seconds time in seconds - * @return _minimumStake minimum quantity of tokens + * @notice Calculates the initial multiplier points (MPs) based on the balance change Δa. The result is equal to + * the amount of balance added. + * @param _amount Represents the change in balance. */ - function _calculateMinimumStake(uint256 _seconds) internal pure returns (uint256 _minimumStake) { - return YEAR / (_seconds * MP_APY); + function _calculateInitialMP(uint256 _amount) public pure returns (uint256 _initialMP) { + return _amount; + } + + /** + * @notice Calculates the reduction in multiplier points (MPs) when a portion of the balance Δa `_reducedAmount` is + * removed from the total balance a_bal `_currentBalance`. + * The reduction is proportional to the ratio of the removed balance to the total balance, applied to the current + * multiplier points $mp$. + * @param _mp Represents the current multiplier points + * @param _currentBalance The total account balance before the removal of Δa `_reducedBalance` + * @param _reducedAmount reduced balance + * @return _reducedMP Multiplier points to reduce from `_mp` + */ + function _calculateReducedMP( + uint256 _mp, + uint256 _currentBalance, + uint256 _reducedAmount + ) + public + pure + returns (uint256 _reducedMP) + { + return Math.mulDiv(_mp, _currentBalance, _reducedAmount); } /** * @notice Calculates maximum stake a given `_amount` can be generated with `MAX_MULTIPLIER` - * @param _amount quantity of tokens + * @param _balance quantity of tokens * @return _maxMPAccured maximum quantity of muliplier points that can be generated for given `_amount` */ - function _calculateMaxAccuredMP(uint256 _amount) internal pure returns (uint256 _maxMPAccured) { - return _calculateAccuredMP(_amount, MAX_MULTIPLIER * YEAR); + function _calculateMaxAccuredMP(uint256 _balance) public pure returns (uint256 _maxMPAccured) { + return Math.mulDiv(_balance, MAX_MULTIPLIER * MP_APY, 100); + } + + /** + * @notice The maximum total multiplier points that can be generated for a determined amount of balance and lock + * duration. + * @param _balance Represents the current account balance + * @param _lockTime The time duration for which the balance is locked + * @return _maxMP Maximum multiplier points that can be generated for given `_balance` and `_lockTime` + */ + function _calculateMaxMP(uint256 _balance, uint256 _lockTime) public pure returns (uint256 _maxMP) { + return _balance + Math.mulDiv(_balance * MP_APY, (MAX_MULTIPLIER * YEAR) + _lockTime, YEAR * 100); + } + + /** + * @dev Caution: This value is estimated and can be incorrect due precision loss. + * @notice Estimates the time an account set as locked time. + * @param _mpMax Maximum multiplier points calculated from the current balance. + * @param _currentBalance Current balance used to calculate the maximum multiplier points. + */ + function _estimateLockTime(uint256 _mpMax, uint256 _currentBalance) public pure returns (uint256 _lockTime) { + return Math.mulDiv((_mpMax - _currentBalance) * 100, YEAR, _currentBalance * MP_APY, Math.Rounding.Ceil) + - MAX_LOCKUP_PERIOD; + } + + /** + * @dev Caution: This value is estimated and can be incorrect due precision loss. + * @notice Calculates the remaining lock time available for a given `_mpMax` and `_currentBalance` + * @param _mpMax Maximum multiplier points calculated from the current balance. + * @param _currentBalance Current balance used to calculate the maximum multiplier points. + */ + function _remainingLockTimeAvailable( + uint256 _mpMax, + uint256 _currentBalance + ) + public + pure + returns (uint256 _lockTime) + { + return Math.mulDiv((_currentBalance * MAX_MULTIPLIER_ABSOLUTE) - _mpMax, YEAR, _currentBalance); + } + + /** + * @notice Calculates the lock time for a given bonus multiplier points and current balance. + * @param _bonusMP bonus multiplier points intended to be generated + * @param _currentBalance current balance + */ + function _calculateLockTime(uint256 _bonusMP, uint256 _currentBalance) public pure returns (uint256 _lockTime) { + return Math.mulDiv(_bonusMP * 100, YEAR, _currentBalance * MP_APY); } } diff --git a/contracts/StakeMath.sol b/contracts/StakeMath.sol new file mode 100644 index 0000000..d427bbc --- /dev/null +++ b/contracts/StakeMath.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT-1.0 +pragma solidity ^0.8.26; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { MultiplierPointMath } from "./MultiplierPointMath.sol"; + +abstract contract StakeMath is MultiplierPointMath { + /// @notice Minimal lockup time + uint256 public constant MIN_LOCKUP_TIME = 1 weeks; + + /** + * @notice Calculates the bonus multiplier points earned when a balance Δa is increased an optionally locked for a + * specified duration + * @param _balance Account current balance + * @param _maxMP Account current max multiplier points + * @param _lockEndTime Account current lock end timestamp + * @param _processTime Process current timestamp + * @param _increasedAmount Increased amount of balance + * @param _increasedLockSeconds Increased amount of seconds to lock + * @return _deltaMpTotal Increased amount of total multiplier points + * @return _newMaxMP Account new max multiplier points + * @return _newLockEnd Account new lock end timestamp + */ + function _calculateStake( + uint256 _balance, + uint256 _maxMP, + uint256 _lockEndTime, + uint256 _processTime, + uint256 _increasedAmount, + uint256 _increasedLockSeconds + ) + public + pure + returns (uint256 _deltaMpTotal, uint256 _newMaxMP, uint256 _newLockEnd) + { + uint256 newBalance = _balance + _increasedAmount; + require(newBalance >= MIN_BALANCE, "StakeMath: balance too low"); + _newLockEnd = Math.max(_lockEndTime, _processTime) + _increasedLockSeconds; + uint256 dt_lock = _newLockEnd - _processTime; + require(dt_lock == 0 || dt_lock >= MIN_LOCKUP_TIME, "StakeMath: lockup time too low"); + require(dt_lock <= MAX_LOCKUP_PERIOD, "StakeMath: lockup time too high"); + + uint256 deltaMpBonus; + if (dt_lock > 0) { + deltaMpBonus = _calculateBonusMP(_increasedAmount, dt_lock); + } + + if (_balance > 0 && _increasedLockSeconds > 0) { + deltaMpBonus += _calculateBonusMP(_balance, _increasedLockSeconds); + } + + _deltaMpTotal = _calculateInitialMP(_increasedAmount) + deltaMpBonus; + _newMaxMP = _maxMP + _deltaMpTotal + _calculateAccuredMP(_balance, MAX_MULTIPLIER * YEAR); + + require( + _newMaxMP <= MAX_MULTIPLIER_ABSOLUTE * (_balance + _increasedAmount), "StakeMath: max multiplier exceeded" + ); + } + + /** + * @notice Calculates the bonus multiplier points earned when a balance Δa is locked for a specified duration + * @param _balance Account current balance + * @param _maxMP Account current max multiplier points + * @param _lockEndTime Account current lock end timestamp + * @param _processTime Process current timestamp + * @param _increasedLockSeconds Increased amount of seconds to lock + * @return _deltaMpTotal Increased amount of total multiplier points + * @return _newMaxMP Account new max multiplier points + * @return _newLockEnd Account new lock end timestamp + */ + function calculateLock( + uint256 _balance, + uint256 _maxMP, + uint256 _lockEndTime, + uint256 _processTime, + uint256 _increasedLockSeconds + ) + public + pure + returns (uint256 _deltaMpTotal, uint256 _newMaxMP, uint256 _newLockEnd) + { + require(_balance > 0); + require(_increasedLockSeconds > 0); + + _newLockEnd = Math.max(_lockEndTime, _processTime) + _increasedLockSeconds; + uint256 dt_lock = _newLockEnd - _processTime; + require(dt_lock == 0 || dt_lock >= MIN_LOCKUP_TIME, "StakeMath: lockup time too low"); + require(dt_lock <= MAX_LOCKUP_PERIOD, "StakeMath: lockup time too high"); + + _deltaMpTotal += _calculateBonusMP(_balance, _increasedLockSeconds); + _newMaxMP = _maxMP + _deltaMpTotal; + + require(_newMaxMP <= MAX_MULTIPLIER_ABSOLUTE * (_balance), "StakeMath: max multiplier exceeded"); + } + + /** + * + * @param _balance Account current balance + * @param _lockEndTime Account current lock end timestamp + * @param _processTime Process current timestamp + * @param _totalMP Account current total multiplier points + * @param _maxMP Account current max multiplier points + * @param _reducedAmount Reduced amount of balance + * @return _deltaMpTotal Increased amount of total multiplier points + * @return _deltaMpMax Increased amount of max multiplier points + */ + function _calculateUnstake( + uint256 _balance, + uint256 _lockEndTime, + uint256 _processTime, + uint256 _totalMP, + uint256 _maxMP, + uint256 _reducedAmount + ) + public + pure + returns (uint256 _deltaMpTotal, uint256 _deltaMpMax) + { + require(_lockEndTime <= _processTime, "StakeMath: lockup not ended"); + require(_balance >= _reducedAmount, "StakeMath: balance too low"); + uint256 newBalance = _balance - _reducedAmount; + require(newBalance == 0 || newBalance >= MIN_BALANCE, "StakeMath: balance too low"); + _deltaMpTotal = _calculateReducedMP(_totalMP, _balance, _reducedAmount); + _deltaMpMax = _calculateReducedMP(_maxMP, _balance, _reducedAmount); + } + + /** + * @notice Calculates the accrued multiplier points for a given balance and seconds passed since last accrual + * @param _balance Account current balance + * @param _totalMP Account current total multiplier points + * @param _maxMP Account current max multiplier points + * @param _lastAccrualTime Account current last accrual timestamp + * @param _processTime Process current timestamp + * @return _deltaMpTotal Increased amount of total multiplier points + */ + function _calculateAccrual( + uint256 _balance, + uint256 _totalMP, + uint256 _maxMP, + uint256 _lastAccrualTime, + uint256 _processTime + ) + public + pure + returns (uint256 _deltaMpTotal) + { + uint256 dt = _processTime - _lastAccrualTime; + require(dt >= ACCURE_RATE, "StakeMath: no enough time passed"); + if (_totalMP <= _maxMP) { + _deltaMpTotal = Math.min(_calculateAccuredMP(_balance, dt), _maxMP - _totalMP); + } + } +}