Skip to content

Commit

Permalink
tbrent Review (#1)
Browse files Browse the repository at this point in the history
* add natspec

* add simplification for case that can never be hit

* add unit notation everywhere

* nit

* final comments

* fix definitions
  • Loading branch information
tbrent authored Apr 25, 2024
1 parent 6d08d9c commit 434bdf9
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 25 deletions.
50 changes: 37 additions & 13 deletions contracts/rewards/GenericMultiRewardsVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

import { RewardInfo, Errors, Events } from "./definitions.sol";

/*
* @title GenericMultiRewardsVault
* @notice Non-transferrable ERC4626 vault that allows streaming of rewards in multiple tokens
* @dev Reward tokens transferred by accident without using fundReward() will be lost!
*
* Registering new reward tokens is permissioned, but adding funds is permissionless
*
* No appreciation; exchange rate is always 1:1 with underlying
*
* Unit notation
* - {qRewardTok} = Reward token quanta
* - {qAsset} = Asset token quanta
* - {qShare} = Share token quanta
* - {s} = Seconds
*/
contract GenericMultiRewardsVault is ERC4626, Ownable {
constructor(
string memory _name,
Expand Down Expand Up @@ -50,6 +65,7 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
super._withdraw(caller, receiver, owner_, assets, shares);
}

/// @dev Prevent transfer
function _update(address from, address to, uint256 amount) internal virtual override {
if (from != address(0) && to != address(0)) {
revert Errors.NotAllowed();
Expand Down Expand Up @@ -91,15 +107,15 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
mapping(IERC20 rewardToken => RewardInfo rewardInfo) public rewardInfos;
mapping(IERC20 rewardToken => address distributor) public distributorInfo;

mapping(address user => mapping(IERC20 rewardToken => uint256 rewardIndex)) public userIndex;
mapping(address user => mapping(IERC20 rewardToken => uint256 accruedRewards)) public accruedRewards;
mapping(address user => mapping(IERC20 rewardToken => uint256 rewardIndex)) public userIndex; // {qRewardTok}
mapping(address user => mapping(IERC20 rewardToken => uint256 accruedRewards)) public accruedRewards; // {qRewardTok}

/**
* @notice Adds a new rewardToken which can be earned via staking. Caller must be owner.
* @param rewardToken Token that can be earned by staking.
* @param distributor Distributor with the ability to control rewards for this token.
* @param rewardsPerSecond The rate in which `rewardToken` will be accrued.
* @param amount Initial funding amount for this reward.
* @param rewardsPerSecond {qRewardTok/s} The rate in which `rewardToken` will be accrued.
* @param amount {qRewardTok} Initial funding amount for this reward.
* @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`.
* @dev If `rewardsPerSecond` is 0 the rewards will be paid out instantly. In this case `amount` must be 0.
*/
Expand Down Expand Up @@ -131,9 +147,6 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
uint8 rewardTokenDecimals = rewardToken.decimals();

uint256 ONE = 10 ** rewardTokenDecimals;
uint256 index = rewardsPerSecond == 0 && amount != 0
? ONE + ((amount * uint256(10 ** decimals())) / totalSupply())
: ONE;
uint48 rewardsEndTimestamp = rewardsPerSecond == 0
? SafeCast.toUint48(block.timestamp)
: _calcRewardsEnd(0, rewardsPerSecond, amount);
Expand All @@ -143,7 +156,7 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
rewardsEndTimestamp: rewardsEndTimestamp,
lastUpdatedTimestamp: SafeCast.toUint48(block.timestamp),
rewardsPerSecond: rewardsPerSecond,
index: index,
index: ONE,
ONE: ONE
});
distributorInfo[rewardToken] = distributor;
Expand Down Expand Up @@ -239,15 +252,21 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
emit Events.RewardInfoUpdate(rewardToken, rewards.rewardsPerSecond, rewardsEndTimestamp);
}

/// @param rewardsEndTimestamp {s}
/// @param rewardsPerSecond {qRewardTok/s}
/// @param amount {qRewardTok}
/// @return {s}
function _calcRewardsEnd(
uint48 rewardsEndTimestamp,
uint256 rewardsPerSecond,
uint256 amount
) internal view returns (uint48) {
if (rewardsEndTimestamp > block.timestamp) {
// {qRewardTok} += ({qRewardTok/s} * ({s} - {s}))
amount += rewardsPerSecond * (rewardsEndTimestamp - block.timestamp);
}

// {s} = {s} + ({qRewardTok} / {qRewardTok/s})
return SafeCast.toUint48(block.timestamp + (amount / uint256(rewardsPerSecond)));
}

Expand Down Expand Up @@ -276,9 +295,8 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
_;
}

/**
* @notice Accrue rewards over time.
*/
/// @notice Accrue rewards over time.
/// @return accrued {qRewardTok}
function _accrueStatic(RewardInfo memory rewards) internal view returns (uint256 accrued) {
uint256 elapsed;
if (rewards.rewardsEndTimestamp > block.timestamp) {
Expand All @@ -287,18 +305,22 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
elapsed = rewards.rewardsEndTimestamp - rewards.lastUpdatedTimestamp;
}

// {qRewardTok} = {qRewardTok/s} * {s}
accrued = uint256(rewards.rewardsPerSecond * elapsed);
}

/// @notice Accrue global rewards for a rewardToken
/// @param accrued {qRewardTok}
function _accrueRewards(IERC20 _rewardToken, uint256 accrued) internal {
uint256 supplyTokens = totalSupply();
uint256 deltaIndex;

if (supplyTokens != 0) {
// {qRewardTok} = {qRewardTok} * {qShare} / {qShare}
deltaIndex = (accrued * uint256(10 ** decimals())) / supplyTokens;
}

// {qRewardTok} += {qRewardTok}
rewardInfos[_rewardToken].index += deltaIndex;
rewardInfos[_rewardToken].lastUpdatedTimestamp = SafeCast.toUint48(block.timestamp);
}
Expand All @@ -318,10 +340,12 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
uint256 deltaIndex = rewardIndex.index - oldIndex;

// Accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed
// {qRewardTok} = {qShare} * {qRewardTok} / {qShare}
uint256 supplierDelta = (balanceOf(_user) * deltaIndex) / uint256(10 ** decimals());
// stakeDecimals * rewardDecimals / stakeDecimals = rewardDecimals

userIndex[_user][_rewardToken] = rewardIndex.index;

// {qRewardTok} += {qRewardTok}
accruedRewards[_user][_rewardToken] += supplierDelta;
userIndex[_user][_rewardToken] = rewardIndex.index;
}
}
8 changes: 4 additions & 4 deletions contracts/rewards/definitions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ pragma solidity 0.8.24;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

struct RewardInfo {
uint8 decimals; // {1} Reward Token Decimals
uint8 decimals; // Reward Token Decimals
uint48 rewardsEndTimestamp; // {s} Rewards End Timestamp; 0 = instant
uint48 lastUpdatedTimestamp; // {s} Last updated timestamp
uint256 rewardsPerSecond; // {1} Rewards per Second
uint256 index; // {1} Last updated reward index
uint256 ONE; // {1} Reward Token Scalar
uint256 rewardsPerSecond; // {qRewardTok/s} Rewards per Second
uint256 index; // {qRewardTok} Last updated reward index
uint256 ONE; // {qRewardTok} Reward Token Scalar
}

abstract contract Errors {
Expand Down
35 changes: 27 additions & 8 deletions contracts/staking/GenericStakedAppreciatingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,40 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s
uint256 constant SCALING_FACTOR = 1e18;

struct RewardTracker {
uint256 rewardPeriodStart;
uint256 rewardPeriodEnd;
uint256 rewardAmount;
uint256 rewardPeriodStart; // {s}
uint256 rewardPeriodEnd; // {s}
uint256 rewardAmount; // {qAsset}
}

/*
* @title GenericMultiRewardsVault
* @notice Transferrable ERC4626 vault with linear reward streaming in the vault's asset token
*
* The only reward token is the asset itself. Adding rewards is permisionless.
*
* Asset tokens accidentally transferred into the contract will be picked up as part of
* the next week distribution period.
*
* Unit notation
* - {qAsset} = Asset token quanta
* - {qShare} = Share token quanta
* - {s} = Seconds
*/
contract GenericStakedAppreciatingVault is ERC4626 {
uint256 public immutable DISTRIBUTION_PERIOD;
uint256 public immutable DISTRIBUTION_PERIOD; // {s}

RewardTracker public rewardTracker;
uint256 private totalDeposited;
uint256 private totalDeposited; // {qAsset}

event RewardAdded(uint256 reward, uint256 periodStart, uint256 periodEnd);
event RewardDistributionUpdated(uint256 periodStart, uint256 periodEnd, uint256 amount);

/// @param _distributionPeriod {s} Distribution Period for Accumulated Rewards
constructor(
string memory _name,
string memory _symbol,
IERC20 _underlying,
uint256 _distributionPeriod // {s} Distribution Period for Accumulated Rewards
uint256 _distributionPeriod
) ERC4626(_underlying) ERC20(_name, _symbol) {
DISTRIBUTION_PERIOD = _distributionPeriod;

Expand All @@ -49,7 +64,6 @@ contract GenericStakedAppreciatingVault is ERC4626 {

uint256 allAvailableAssets = _asset.balanceOf(address(this));
uint256 rewardsToBeDistributed = allAvailableAssets - totalDeposited;

rewardTracker.rewardPeriodEnd = block.timestamp + DISTRIBUTION_PERIOD;
rewardTracker.rewardAmount = rewardsToBeDistributed;
}
Expand All @@ -64,7 +78,9 @@ contract GenericStakedAppreciatingVault is ERC4626 {
);
}

/// @return {qAsset}
function totalAssets() public view override returns (uint256) {
// {qAsset} = {qAsset} + {qAsset}
return totalDeposited + _currentAccountedRewards();
}

Expand All @@ -80,17 +96,20 @@ contract GenericStakedAppreciatingVault is ERC4626 {
emit RewardAdded(rewardTracker.rewardAmount, rewardTracker.rewardPeriodStart, rewardTracker.rewardPeriodEnd);
}

/// @return {qAsset}
function _currentAccountedRewards() internal view returns (uint256) {
if (block.timestamp >= rewardTracker.rewardPeriodEnd) {
return rewardTracker.rewardAmount;
}

uint256 previousDistributionPeriod = rewardTracker.rewardPeriodEnd - rewardTracker.rewardPeriodStart;
uint256 timePassed = block.timestamp - rewardTracker.rewardPeriodStart;

// D18{1} = {s} * D18 / {s}
uint256 timePassedPercentage = (timePassed * SCALING_FACTOR) / previousDistributionPeriod;

// {qAsset} = {qAsset} * D18{1} / D18
uint256 accountedRewards = (rewardTracker.rewardAmount * timePassedPercentage) / SCALING_FACTOR;

return accountedRewards;
}

Expand Down

0 comments on commit 434bdf9

Please sign in to comment.