Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] update Vault and RewardsDistributor logic with IStrategyManager #5

Merged
merged 18 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ docs/
# Dotenv file
.env
node_modules

.idea
183 changes: 23 additions & 160 deletions contracts/RewardsDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.
import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol";
import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol";
import {IVault} from "./interfaces/IVault.sol";
import {IStrategyManager} from "./interfaces/IStrategyManager.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/*
* @title Curve Fee Distribution modified for ve(3,3) emissions
Expand All @@ -14,181 +16,42 @@ import {IVault} from "./interfaces/IVault.sol";
* @license MIT
*/
contract RewardsDistributor is IRewardsDistributor, ReentrancyGuard {
/// @inheritdoc IRewardsDistributor
uint256 public constant WEEK = 7 * 86400;

/// @inheritdoc IRewardsDistributor
uint256 public startTime;
/// @inheritdoc IRewardsDistributor
mapping(uint256 => uint256) public timeCursorOf;
using SafeERC20 for IERC20;

/// @inheritdoc IRewardsDistributor
uint256 public lastTokenTime;
uint256[1000000000000000] public tokensPerWeek;
/// @inheritdoc IStrategyManager
IStrategyManager public strategyManager;

/// @inheritdoc IRewardsDistributor
IVotingEscrow public immutable ve;
/// @inheritdoc IRewardsDistributor
address public vault;
/// @inheritdoc IRewardsDistributor
uint256 public tokenLastBalance;

constructor(address _ve) {
uint256 _t = (block.timestamp / WEEK) * WEEK;
startTime = _t;
lastTokenTime = _t;
ve = IVotingEscrow(_ve);
constructor(address manager) {
vault = msg.sender;
strategyManager = IStrategyManager(manager);
}

receive() external payable {}

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 {
/// @inheritdoc distribute rewards in native token
function distributeRewards() external payable {
if (msg.sender != vault) revert NotVault();
_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;
uint256 _amount = msg.value;
if (_amount == 0) revert ZeroAmount();
strategyManager.rewards{value: _amount}(_amount);
}

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 nonReentrant returns (uint256) {
if (IVault(vault).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 (
ve.lockedToken(_tokenId) != address(0) ||
ve.tokenIdNative(_tokenId) != 0 ||
(_timestamp >= _locked.end && !_locked.isPermanent)
) {
address _owner = ve.ownerOf(_tokenId);
(bool success, ) = payable(_owner).call{value: amount}("");
require(success, "transfer rewards failed.");
} else {
ve.depositFor{value: amount}(_tokenId, amount);
}
tokenLastBalance -= amount;
}
return amount;
/// @inheritdoc distribute rewards in erc20 format
function distributeRewards(address _token, uint256 _amount) external nonReentrant{
if (msg.sender != vault) revert NotVault();
if (_amount == 0) revert ZeroAmount();
IERC20(_token).safeTransfer(address(strategyManager), _amount);
strategyManager.rewards(_token, _amount);
}

/// @inheritdoc IRewardsDistributor
function claimMany(uint256[] calldata _tokenIds) external nonReentrant returns (bool) {
if (IVault(vault).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 (
ve.lockedToken(_tokenId) != address(0) ||
ve.tokenIdNative(_tokenId) != 0 ||
(_timestamp >= _locked.end && !_locked.isPermanent)
) {
address _owner = ve.ownerOf(_tokenId);
(bool success, ) = payable(_owner).call{value: amount}("");
require(success, "transfer rewards failed.");
} else {
ve.depositFor{value: amount}(_tokenId, amount);
}
total += amount;
}
}
if (total != 0) {
tokenLastBalance -= total;
}

return true;
/// @inheritdoc update strategyManager
function setStrategyManager(address _manager) external {
if (msg.sender != vault) revert NotVault();
if (_manager == address(0)) revert ZeroAddress();
strategyManager = _manager;
}

/// @inheritdoc IRewardsDistributor
Expand Down
9 changes: 1 addition & 8 deletions contracts/Vault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini
import {IVault} from "./interfaces/IVault.sol";
import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol";
import {IVoter} from "./interfaces/IVoter.sol";
import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol";

/// @title Vault
/// @notice Controls minting of emissions and rebases for the Protocol
Expand All @@ -20,8 +19,6 @@ contract Vault is IVault, Initializable {
/// @inheritdoc IVault
address public governor;
/// @inheritdoc IVault
IVotingEscrow public ve;
/// @inheritdoc IVault
IRewardsDistributor public rewardsDistributor;

/// @inheritdoc IVault
Expand All @@ -37,11 +34,9 @@ contract Vault is IVault, Initializable {

function initialize(
address _voter, // the voting & distribution system
address _ve, // the ve(3,3) system that will be locked into
address _rewardsDistributor // the distribution system that ensures users aren't diluted
) public initializer {
voter = IVoter(_voter);
ve = IVotingEscrow(_ve);
governor = msg.sender;
rewardsDistributor = IRewardsDistributor(_rewardsDistributor);
veRate = 1000;
Expand Down Expand Up @@ -74,9 +69,7 @@ contract Vault is IVault, Initializable {
}
uint256 _veEmission = (_emission * veRate) / 10000;

payable(address(rewardsDistributor)).transfer(_veEmission);
rewardsDistributor.checkpointToken(); // checkpoint token balance that was just minted in rewards distributor

rewardsDistributor.distributeRewards{value: _veEmission}();
voter.notifyRewardAmount{value: _emission - _veEmission}();

emit Emission(msg.sender, _emission);
Expand Down
50 changes: 10 additions & 40 deletions contracts/interfaces/IRewardsDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,27 @@ 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 NotVault();
error NotManagedOrNormalNFT();
error UpdatePeriod();
error ZeroAddress();
error ZeroAmount();

/// @notice 7 days in seconds
function WEEK() external view returns (uint256);
/// @notice distribute rewards with native token to strategyManager.
function distributeRewards() external payable;

/// @notice Timestamp of contract creation
function startTime() external view returns (uint256);
/// @notice distribute rewards with erc20 to strategyManager.
function distributeRewards(address _token, uint256 _amount) external;

/// @notice Timestamp of most recent claim of tokenId
function timeCursorOf(uint256 tokenId) external view returns (uint256);
/// @notice update strategy manager only vault.
function setStrategyManager(address _manager) external;

/// @notice The last timestamp Vault has called checkpointToken()
function lastTokenTime() external view returns (uint256);

/// @notice Interface of VotingEscrow.sol
function ve() external view returns (IVotingEscrow);
/// @notice
function strategyManager() external view returns (address);

/// @notice Address of Vault.sol
/// Authorized caller of checkpointToken()
function vault() 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
/// `Vault.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 `Vault.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 vault once on initialization
/// @dev Callable once by vault only, Vault is immutable
function setVault(address _vault) external;
Expand Down
62 changes: 62 additions & 0 deletions contracts/interfaces/IStrategyManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IStrategy.sol";

interface IStrategyManager {

/**
* @notice change strategy ratio
*/
function changeStrategyRatio(address strategy, uint256 ratio) external;

/**
* @notice convenience function for fetching the total shares of `user`
*/
function shares(address user) external view returns (uint256);

/**
* @notice convenience function for fetching the total shares of `user` at a specific moment in the past.
*/
function shares(address user, uint256 timepoint) external view returns (uint256);

/**
* @notice convenience function for fetching the total shares of `user`
*/
function shares(address user, address strategy) external view returns (uint256);

/**
* @notice convenience function for fetching the total shares of `user` at a specific moment in the past.
*/
function shares(address user, address strategy, uint256 timepoint) external view returns (uint256);

/**
* @notice The total number of extant shares
*/
function totalShares() external view returns (uint256);

/**
* @notice The total number of extant shares at a specific moment in the past.
*/
function totalShares(uint256 timepoint) external view returns (uint256);

/**
* @notice The total number of extant shares
*/
function totalShares(address strategy) external view returns (uint256);

/**
* @notice The total number of extant shares at a specific moment in the past.
*/
function totalShares(address strategy, uint256 timepoint) external view returns (uint256);
ludete marked this conversation as resolved.
Show resolved Hide resolved

/**
* @notice Distribute rewards with native token to strategyManager.
*/
function rewards(uint256 amount) external payable;
ludete marked this conversation as resolved.
Show resolved Hide resolved

/**
* @notice Distribute rewards with erc20 token to strategyManager.
*/
function rewards(address token, uint256 amount) external returns(bool);
ludete marked this conversation as resolved.
Show resolved Hide resolved
}
Loading