Skip to content

Commit

Permalink
add rewards distributor contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
ququzone committed Jan 13, 2024
1 parent bc9c0de commit 742137c
Show file tree
Hide file tree
Showing 17 changed files with 2,296 additions and 2 deletions.
186 changes: 186 additions & 0 deletions contracts/RewardsDistributor.sol
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;
}
}
83 changes: 83 additions & 0 deletions contracts/interfaces/IMinter.sol
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);
}
61 changes: 61 additions & 0 deletions contracts/interfaces/IRewardsDistributor.sol
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;
}
Loading

0 comments on commit 742137c

Please sign in to comment.