-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
2,296 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
pragma solidity 0.8.19; | ||
|
||
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; | ||
import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol"; | ||
import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol"; | ||
import {IMinter} from "./interfaces/IMinter.sol"; | ||
|
||
/* | ||
* @title Curve Fee Distribution modified for ve(3,3) emissions | ||
* @author Curve Finance, andrecronje | ||
* @author velodrome.finance, @figs999, @pegahcarter | ||
* @license MIT | ||
*/ | ||
contract RewardsDistributor is IRewardsDistributor { | ||
/// @inheritdoc IRewardsDistributor | ||
uint256 public constant WEEK = 7 * 86400; | ||
|
||
/// @inheritdoc IRewardsDistributor | ||
uint256 public startTime; | ||
/// @inheritdoc IRewardsDistributor | ||
mapping(uint256 => uint256) public timeCursorOf; | ||
|
||
/// @inheritdoc IRewardsDistributor | ||
uint256 public lastTokenTime; | ||
uint256[1000000000000000] public tokensPerWeek; | ||
|
||
/// @inheritdoc IRewardsDistributor | ||
IVotingEscrow public immutable ve; | ||
/// @inheritdoc IRewardsDistributor | ||
address public minter; | ||
/// @inheritdoc IRewardsDistributor | ||
uint256 public tokenLastBalance; | ||
|
||
constructor(address _ve) { | ||
uint256 _t = (block.timestamp / WEEK) * WEEK; | ||
startTime = _t; | ||
lastTokenTime = _t; | ||
ve = IVotingEscrow(_ve); | ||
minter = msg.sender; | ||
} | ||
|
||
function _checkpointToken() internal { | ||
uint256 tokenBalance = address(this).balance; | ||
uint256 toDistribute = tokenBalance - tokenLastBalance; | ||
tokenLastBalance = tokenBalance; | ||
|
||
uint256 t = lastTokenTime; | ||
uint256 sinceLast = block.timestamp - t; | ||
lastTokenTime = block.timestamp; | ||
uint256 thisWeek = (t / WEEK) * WEEK; | ||
uint256 nextWeek = 0; | ||
uint256 timestamp = block.timestamp; | ||
|
||
for (uint256 i = 0; i < 20; i++) { | ||
nextWeek = thisWeek + WEEK; | ||
if (timestamp < nextWeek) { | ||
if (sinceLast == 0 && timestamp == t) { | ||
tokensPerWeek[thisWeek] += toDistribute; | ||
} else { | ||
tokensPerWeek[thisWeek] += (toDistribute * (timestamp - t)) / sinceLast; | ||
} | ||
break; | ||
} else { | ||
if (sinceLast == 0 && nextWeek == t) { | ||
tokensPerWeek[thisWeek] += toDistribute; | ||
} else { | ||
tokensPerWeek[thisWeek] += (toDistribute * (nextWeek - t)) / sinceLast; | ||
} | ||
} | ||
t = nextWeek; | ||
thisWeek = nextWeek; | ||
} | ||
emit CheckpointToken(timestamp, toDistribute); | ||
} | ||
|
||
/// @inheritdoc IRewardsDistributor | ||
function checkpointToken() external { | ||
if (msg.sender != minter) revert NotMinter(); | ||
_checkpointToken(); | ||
} | ||
|
||
function _claim(uint256 _tokenId, uint256 _lastTokenTime) internal returns (uint256) { | ||
(uint256 toDistribute, uint256 epochStart, uint256 weekCursor) = _claimable(_tokenId, _lastTokenTime); | ||
timeCursorOf[_tokenId] = weekCursor; | ||
if (toDistribute == 0) return 0; | ||
|
||
emit Claimed(_tokenId, epochStart, weekCursor, toDistribute); | ||
return toDistribute; | ||
} | ||
|
||
function _claimable( | ||
uint256 _tokenId, | ||
uint256 _lastTokenTime | ||
) internal view returns (uint256 toDistribute, uint256 weekCursorStart, uint256 weekCursor) { | ||
uint256 _startTime = startTime; | ||
weekCursor = timeCursorOf[_tokenId]; | ||
weekCursorStart = weekCursor; | ||
|
||
// case where token does not exist | ||
uint256 maxUserEpoch = ve.userPointEpoch(_tokenId); | ||
if (maxUserEpoch == 0) return (0, weekCursorStart, weekCursor); | ||
|
||
// case where token exists but has never been claimed | ||
if (weekCursor == 0) { | ||
IVotingEscrow.UserPoint memory userPoint = ve.userPointHistory(_tokenId, 1); | ||
weekCursor = (userPoint.ts / WEEK) * WEEK; | ||
weekCursorStart = weekCursor; | ||
} | ||
if (weekCursor >= _lastTokenTime) return (0, weekCursorStart, weekCursor); | ||
if (weekCursor < _startTime) weekCursor = _startTime; | ||
|
||
for (uint256 i = 0; i < 50; i++) { | ||
if (weekCursor >= _lastTokenTime) break; | ||
|
||
uint256 balance = ve.balanceOfNFTAt(_tokenId, weekCursor + WEEK - 1); | ||
uint256 supply = ve.totalSupplyAt(weekCursor + WEEK - 1); | ||
supply = supply == 0 ? 1 : supply; | ||
toDistribute += (balance * tokensPerWeek[weekCursor]) / supply; | ||
weekCursor += WEEK; | ||
} | ||
} | ||
|
||
/// @inheritdoc IRewardsDistributor | ||
function claimable(uint256 _tokenId) external view returns (uint256 claimable_) { | ||
uint256 _lastTokenTime = (lastTokenTime / WEEK) * WEEK; | ||
(claimable_, , ) = _claimable(_tokenId, _lastTokenTime); | ||
} | ||
|
||
/// @inheritdoc IRewardsDistributor | ||
function claim(uint256 _tokenId) external returns (uint256) { | ||
if (IMinter(minter).activePeriod() < ((block.timestamp / WEEK) * WEEK)) revert UpdatePeriod(); | ||
uint256 _timestamp = block.timestamp; | ||
uint256 _lastTokenTime = lastTokenTime; | ||
_lastTokenTime = (_lastTokenTime / WEEK) * WEEK; | ||
uint256 amount = _claim(_tokenId, _lastTokenTime); | ||
if (amount != 0) { | ||
IVotingEscrow.LockedBalance memory _locked = ve.locked(_tokenId); | ||
if ((_timestamp >= _locked.end && !_locked.isPermanent) || ve.lockedToken(_tokenId) != address(0)) { | ||
address _owner = ve.ownerOf(_tokenId); | ||
payable(_owner).transfer(amount); | ||
} else { | ||
ve.depositFor{value: amount}(_tokenId, amount); | ||
} | ||
tokenLastBalance -= amount; | ||
} | ||
return amount; | ||
} | ||
|
||
/// @inheritdoc IRewardsDistributor | ||
function claimMany(uint256[] calldata _tokenIds) external returns (bool) { | ||
if (IMinter(minter).activePeriod() < ((block.timestamp / WEEK) * WEEK)) revert UpdatePeriod(); | ||
uint256 _timestamp = block.timestamp; | ||
uint256 _lastTokenTime = lastTokenTime; | ||
_lastTokenTime = (_lastTokenTime / WEEK) * WEEK; | ||
uint256 total = 0; | ||
uint256 _length = _tokenIds.length; | ||
|
||
for (uint256 i = 0; i < _length; i++) { | ||
uint256 _tokenId = _tokenIds[i]; | ||
if (_tokenId == 0) break; | ||
uint256 amount = _claim(_tokenId, _lastTokenTime); | ||
if (amount != 0) { | ||
IVotingEscrow.LockedBalance memory _locked = ve.locked(_tokenId); | ||
if ((_timestamp >= _locked.end && !_locked.isPermanent) || ve.lockedToken(_tokenId) != address(0)) { | ||
address _owner = ve.ownerOf(_tokenId); | ||
payable(_owner).transfer(amount); | ||
} else { | ||
ve.depositFor{value: amount}(_tokenId, amount); | ||
} | ||
total += amount; | ||
} | ||
} | ||
if (total != 0) { | ||
tokenLastBalance -= total; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/// @inheritdoc IRewardsDistributor | ||
function setMinter(address _minter) external { | ||
if (msg.sender != minter) revert NotMinter(); | ||
minter = _minter; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import {IVoter} from "./IVoter.sol"; | ||
import {IVotingEscrow} from "./IVotingEscrow.sol"; | ||
import {IRewardsDistributor} from "./IRewardsDistributor.sol"; | ||
|
||
interface IMinter { | ||
error AlreadyNudged(); | ||
error NotEpochGovernor(); | ||
error TailEmissionsInactive(); | ||
|
||
event Mint(address indexed _sender, uint256 _weekly, uint256 _circulating_supply, bool indexed _tail); | ||
event Nudge(uint256 indexed _period, uint256 _oldRate, uint256 _newRate); | ||
|
||
/// @notice Interface of Voter.sol | ||
function voter() external view returns (IVoter); | ||
|
||
/// @notice Interface of IVotingEscrow.sol | ||
function ve() external view returns (IVotingEscrow); | ||
|
||
/// @notice Interface of RewardsDistributor.sol | ||
function rewardsDistributor() external view returns (IRewardsDistributor); | ||
|
||
/// @notice Duration of epoch in seconds | ||
function WEEK() external view returns (uint256); | ||
|
||
/// @notice Decay rate of emissions as percentage of `MAX_BPS` | ||
function WEEKLY_DECAY() external view returns (uint256); | ||
|
||
/// @notice Maximum tail emission rate in basis points. | ||
function MAXIMUM_TAIL_RATE() external view returns (uint256); | ||
|
||
/// @notice Minimum tail emission rate in basis points. | ||
function MINIMUM_TAIL_RATE() external view returns (uint256); | ||
|
||
/// @notice Denominator for emissions calculations (as basis points) | ||
function MAX_BPS() external view returns (uint256); | ||
|
||
/// @notice Rate change per proposal | ||
function NUDGE() external view returns (uint256); | ||
|
||
/// @notice When emissions fall below this amount, begin tail emissions | ||
function TAIL_START() external view returns (uint256); | ||
|
||
/// @notice Tail emissions rate in basis points | ||
function tailEmissionRate() external view returns (uint256); | ||
|
||
/// @notice Starting weekly emission of 15M VELO (VELO has 18 decimals) | ||
function weekly() external view returns (uint256); | ||
|
||
/// @notice Timestamp of start of epoch that updatePeriod was last called in | ||
function activePeriod() external returns (uint256); | ||
|
||
/// @dev activePeriod => proposal existing, used to enforce one proposal per epoch | ||
/// @param _activePeriod Timestamp of start of epoch | ||
/// @return True if proposal has been executed, else false | ||
function proposals(uint256 _activePeriod) external view returns (bool); | ||
|
||
/// @notice Allows epoch governor to modify the tail emission rate by at most 1 basis point | ||
/// per epoch to a maximum of 100 basis points or to a minimum of 1 basis point. | ||
/// Note: the very first nudge proposal must take place the week prior | ||
/// to the tail emission schedule starting. | ||
/// @dev Throws if not epoch governor. | ||
/// Throws if not currently in tail emission schedule. | ||
/// Throws if already nudged this epoch. | ||
/// Throws if nudging above maximum rate. | ||
/// Throws if nudging below minimum rate. | ||
/// This contract is coupled to EpochGovernor as it requires three option simple majority voting. | ||
function nudge() external; | ||
|
||
/// @notice Calculates rebases according to the formula | ||
/// weekly * (ve.totalSupply / velo.totalSupply) ^ 3 / 2 | ||
/// Note that ve.totalSupply is the locked ve supply | ||
/// velo.totalSupply is the total ve supply minted | ||
/// @param _minted Amount of VELO minted this epoch | ||
/// @return _growth Rebases | ||
function calculateGrowth(uint256 _minted) external view returns (uint256 _growth); | ||
|
||
/// @notice Processes emissions and rebases. Callable once per epoch (1 week). | ||
/// @return _period Start of current epoch. | ||
function updatePeriod() external returns (uint256 _period); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import {IVotingEscrow} from "./IVotingEscrow.sol"; | ||
|
||
interface IRewardsDistributor { | ||
event CheckpointToken(uint256 time, uint256 tokens); | ||
event Claimed(uint256 indexed tokenId, uint256 indexed epochStart, uint256 indexed epochEnd, uint256 amount); | ||
|
||
error NotMinter(); | ||
error NotManagedOrNormalNFT(); | ||
error UpdatePeriod(); | ||
|
||
/// @notice 7 days in seconds | ||
function WEEK() external view returns (uint256); | ||
|
||
/// @notice Timestamp of contract creation | ||
function startTime() external view returns (uint256); | ||
|
||
/// @notice Timestamp of most recent claim of tokenId | ||
function timeCursorOf(uint256 tokenId) external view returns (uint256); | ||
|
||
/// @notice The last timestamp Minter has called checkpointToken() | ||
function lastTokenTime() external view returns (uint256); | ||
|
||
/// @notice Interface of VotingEscrow.sol | ||
function ve() external view returns (IVotingEscrow); | ||
|
||
/// @notice Address of Minter.sol | ||
/// Authorized caller of checkpointToken() | ||
function minter() external view returns (address); | ||
|
||
/// @notice Amount of token in contract when checkpointToken() was last called | ||
function tokenLastBalance() external view returns (uint256); | ||
|
||
/// @notice Called by Minter to notify Distributor of rebases | ||
function checkpointToken() external; | ||
|
||
/// @notice Returns the amount of rebases claimable for a given token ID | ||
/// @dev Allows claiming of rebases up to 50 epochs old | ||
/// @param tokenId The token ID to check | ||
/// @return The amount of rebases claimable for the given token ID | ||
function claimable(uint256 tokenId) external view returns (uint256); | ||
|
||
/// @notice Claims rebases for a given token ID | ||
/// @dev Allows claiming of rebases up to 50 epochs old | ||
/// `Minter.updatePeriod()` must be called before claiming | ||
/// @param tokenId The token ID to claim for | ||
/// @return The amount of rebases claimed | ||
function claim(uint256 tokenId) external returns (uint256); | ||
|
||
/// @notice Claims rebases for a list of token IDs | ||
/// @dev `Minter.updatePeriod()` must be called before claiming | ||
/// @param tokenIds The token IDs to claim for | ||
/// @return Whether or not the claim succeeded | ||
function claimMany(uint256[] calldata tokenIds) external returns (bool); | ||
|
||
/// @notice Used to set minter once on initialization | ||
/// @dev Callable once by Minter only, Minter is immutable | ||
function setMinter(address _minter) external; | ||
} |
Oops, something went wrong.