Skip to content

Commit

Permalink
Should be done
Browse files Browse the repository at this point in the history
  • Loading branch information
akshatmittal committed Apr 25, 2024
1 parent 9b3e12c commit 4079513
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 240 deletions.
64 changes: 46 additions & 18 deletions contracts/rewards/GenericMultiRewardsVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
address initialOwner
) ERC4626(_underlying) ERC20(_name, _symbol) Ownable(initialOwner) {}

/*
** Core Vault Functionality
*/

function _convertToShares(uint256 assets, Math.Rounding) internal pure override returns (uint256) {
return assets;
}
Expand Down Expand Up @@ -54,9 +58,9 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
super._update(from, to, amount);
}

/*//////////////////////////////////////////////////////////////
CLAIM LOGIC
//////////////////////////////////////////////////////////////*/
/*
** Rewards: Claim Logic
*/

/**
* @notice Claim rewards for a user in any amount of rewardTokens.
Expand All @@ -79,26 +83,32 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
}
}

/*//////////////////////////////////////////////////////////////
REWARDS MANAGEMENT LOGIC
//////////////////////////////////////////////////////////////*/
/*
** Rewards: Management
*/

IERC20[] public rewardTokens;
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;

/**
* @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.
* @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.
* @dev If `useEscrow` is `false` the `escrowDuration`, `escrowPercentage` and `offset` will be ignored.
*/
function addRewardToken(IERC20Metadata rewardToken, uint256 rewardsPerSecond, uint256 amount) external onlyOwner {
function addRewardToken(
IERC20Metadata rewardToken,
address distributor,
uint256 rewardsPerSecond,
uint256 amount
) external onlyOwner {
if (asset() == address(rewardToken)) {
revert Errors.RewardTokenCanNotBeStakingToken();
}
Expand Down Expand Up @@ -136,17 +146,39 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
index: index,
ONE: ONE
});
distributorInfo[rewardToken] = distributor;

emit Events.RewardInfoUpdate(rewardToken, rewardsPerSecond, rewardsEndTimestamp);
}

/**
* @notice Changes rewards speed for a rewardToken. This works only for rewards that accrue over time. Caller must be owner.
* @notice Updates distributor for the rewardToken
* @param rewardToken Token that can be earned by staking.
* @param distributor Distributor with the ability to control rewards for this token.
* @dev Callable by owner or distributor themselves.
* @dev Setting to address(0) will only allow owner to control it.
*/
function updateDistributor(IERC20 rewardToken, address distributor) external {
if (_msgSender() != owner() && _msgSender() != distributorInfo[rewardToken]) {
revert Errors.InvalidCaller(_msgSender());
}

distributorInfo[rewardToken] = distributor;
}

/**
* @notice Changes `rewardsPerSecond` for rewardToken.
* @param rewardToken Token that can be earned by staking.
* @param rewardsPerSecond The rate in which `rewardToken` will be accrued.
* @dev Callable by owner or distributor for the token.
* @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`.
* @dev Only for rewards that accrue over time.
*/
function changeRewardSpeed(IERC20 rewardToken, uint256 rewardsPerSecond) external onlyOwner {
function changeRewardSpeed(IERC20 rewardToken, uint256 rewardsPerSecond) external {
if (_msgSender() != owner() && _msgSender() != distributorInfo[rewardToken]) {
revert Errors.InvalidCaller(_msgSender());
}

RewardInfo memory rewards = rewardInfos[rewardToken];

if (rewardsPerSecond == 0) {
Expand All @@ -172,11 +204,12 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
}

/**
* @notice Funds rewards for a rewardToken.
* @notice Fund reward streams for a rewardToken.
* @param rewardToken Token that can be earned by staking.
* @param amount The amount of rewardToken that will fund this reward.
* @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`.
* @dev If `rewardsPerSecond` is 0 the rewards will be paid out instantly.
* @dev Permissionless
*/
function fundReward(IERC20 rewardToken, uint256 amount) external {
if (amount == 0) {
Expand Down Expand Up @@ -222,11 +255,6 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {
return rewardTokens;
}

/*//////////////////////////////////////////////////////////////
REWARDS ACCRUAL LOGIC
//////////////////////////////////////////////////////////////*/

/// @notice Accrue rewards for up to 2 users for all available reward tokens.
modifier accrueRewards(address _caller, address _receiver) {
IERC20[] memory _rewardTokens = rewardTokens;
for (uint256 i; i < _rewardTokens.length; i++) {
Expand All @@ -239,7 +267,8 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {

_accrueUser(_receiver, rewardToken);

// If a deposit/withdraw operation gets called for another user we should accrue for both of them to avoid potential issues
// If a deposit/withdraw operation gets called for another user we should
// accrue for both of them to avoid potential issues
if (_receiver != _caller) {
_accrueUser(_caller, rewardToken);
}
Expand All @@ -249,7 +278,6 @@ contract GenericMultiRewardsVault is ERC4626, Ownable {

/**
* @notice Accrue rewards over time.
* @dev Based on https://github.com/fei-protocol/flywheel-v2/blob/main/src/rewards/FlywheelStaticRewards.sol
*/
function _accrueStatic(RewardInfo memory rewards) internal view returns (uint256 accrued) {
uint256 elapsed;
Expand Down
1 change: 1 addition & 0 deletions contracts/rewards/definitions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ abstract contract Errors {
error RewardsAreDynamic(IERC20 rewardToken);
error ZeroRewardsSpeed();
error InvalidConfig();
error InvalidCaller(address caller);

// Transfers
error NotAllowed();
Expand Down
9 changes: 7 additions & 2 deletions contracts/staking/GenericStakedAppreciatingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pragma solidity 0.8.24;
import { ERC4626, IERC20, ERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// TODO: Move these to a common definitions file
uint256 constant SCALING_FACTOR = 1e18;

struct RewardTracker {
Expand All @@ -20,6 +19,7 @@ contract GenericStakedAppreciatingVault is ERC4626 {
uint256 private totalDeposited;

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

constructor(
string memory _name,
Expand All @@ -33,7 +33,6 @@ contract GenericStakedAppreciatingVault is ERC4626 {
}

function _updateRewards() internal {
// TODO: No tracking when totalSupply() == 0
IERC20 _asset = IERC20(asset());

uint256 accountedRewards = _currentAccountedRewards();
Expand All @@ -57,6 +56,12 @@ contract GenericStakedAppreciatingVault is ERC4626 {

// Either way, we're tracking the distribution from now.
rewardTracker.rewardPeriodStart = block.timestamp;

emit RewardDistributionUpdated(
rewardTracker.rewardPeriodStart,
rewardTracker.rewardPeriodEnd,
rewardTracker.rewardAmount
);
}

function totalAssets() public view override returns (uint256) {
Expand Down
24 changes: 12 additions & 12 deletions test/GenericMultiRewardsVault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -483,11 +483,11 @@ contract GenericMultiRewardsVaultTest is Test {
uint256 totalAmount = 10 * ONE;
uint256 rewardsPerSecond = totalAmount / 100; // so, duration = 100

staking.addRewardToken(IERC20Metadata(address(rewardsToken)), rewardsPerSecond, totalAmount);
staking.addRewardToken(IERC20Metadata(address(rewardsToken)), address(this), rewardsPerSecond, totalAmount);
}

function _addRewardTokenWithZeroRewardsSpeed(ERC20Mock rewardsToken) internal {
staking.addRewardToken(IERC20Metadata(address(rewardsToken)), 0, 0);
staking.addRewardToken(IERC20Metadata(address(rewardsToken)), address(this), 0, 0);
}

function test__addRewardToken() public {
Expand All @@ -500,7 +500,7 @@ contract GenericMultiRewardsVaultTest is Test {

emit Events.RewardInfoUpdate(iRewardToken1, 0.1 ether, SafeCast.toUint32(callTimestamp + 100));

staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0.1 ether, 10 ether);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0.1 ether, 10 ether);

// Confirm that all data is set correctly
IERC20[] memory rewardTokens = staking.getAllRewardsTokens();
Expand Down Expand Up @@ -531,7 +531,7 @@ contract GenericMultiRewardsVaultTest is Test {
vm.expectEmit(false, false, false, true, address(staking));
emit Events.RewardInfoUpdate(iRewardToken1, 0, SafeCast.toUint32(callTimestamp));

staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0, 0);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0, 0);

(
uint8 decimals,
Expand All @@ -557,7 +557,7 @@ contract GenericMultiRewardsVaultTest is Test {
rewardToken1.transfer(address(staking), 10 ether);

uint256 callTimestamp = block.timestamp;
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0.1 ether, 10 ether);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0.1 ether, 10 ether);

// RewardsEndTimeStamp shouldnt be affected by previous token transfer
(, uint48 rewardsEndTimestamp, , , , ) = staking.rewardInfos(iRewardToken1);
Expand All @@ -573,34 +573,34 @@ contract GenericMultiRewardsVaultTest is Test {
rewardToken1.mint(address(this), 20 ether);
rewardToken1.approve(address(staking), 20 ether);

staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0.1 ether, 10 ether);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0.1 ether, 10 ether);

vm.expectRevert(Errors.RewardTokenAlreadyExist.selector);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0.1 ether, 10 ether);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0.1 ether, 10 ether);
}

function testFail__addRewardToken_rewardsToken_is_stakingToken() public {
staking.addRewardToken(IERC20Metadata(address(stakingToken)), 0.1 ether, 10 ether);
staking.addRewardToken(IERC20Metadata(address(stakingToken)), address(this), 0.1 ether, 10 ether);
}

function testFail__addRewardToken_0_rewardsSpeed_non_0_amount() public {
// Prepare to transfer reward tokens
rewardToken1.mint(address(this), 1 ether);
rewardToken1.approve(address(staking), 1 ether);

staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0, 1 ether);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0, 1 ether);
}

function testFail__addRewardToken_escrow_with_0_percentage() public {
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0.1 ether, 10 ether);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0.1 ether, 10 ether);
}

function testFail__addRewardToken_escrow_with_more_than_100_percentage() public {
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0.1 ether, 10 ether);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0.1 ether, 10 ether);
}

function testFail__addRewardToken_0_rewardsSpeed_amount_larger_0_and_0_shares() public {
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), 0, 10);
staking.addRewardToken(IERC20Metadata(address(iRewardToken1)), address(this), 0, 10);
}

/*//////////////////////////////////////////////////////////////
Expand Down
68 changes: 68 additions & 0 deletions test/GenericStakedAppreciatingVault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,72 @@ contract GenericStakedAppreciatingVaultTest is Test {
// After 1 week, we should expect to get 800 tokens for our 100 tokens staked.
assertApproxEqAbs(vault.previewRedeem(100 * 10 ** vaultDecimals), 800 * 10 ** tokenDecimals, maxError);
}

function test_Distribution_MultipleDeposits() public {
_stakeAs(USER1, 100 * 10 ** tokenDecimals);

_addRewardsAs(DEPLOYER, 700 * 10 ** tokenDecimals);

_triggerNextPeriod();

vm.warp(block.timestamp + 3.5 days);

_stakeAs(USER2, 100 * 10 ** tokenDecimals);

assertApproxEqAbs(vault.balanceOf(USER1), 100 * 10 ** vaultDecimals, maxError);
assertApproxEqAbs(vault.balanceOf(USER2), (200 * 10 ** vaultDecimals) / 9, maxError); // 22.22222

vm.warp(block.timestamp + 3.5 days);
vm.warp(block.timestamp + 1);

assertApproxEqAbs(vault.previewRedeem(100 * 10 ** vaultDecimals), 736 * 10 ** tokenDecimals, 1e18);
}

function test_Distribution_MultipleDepositRedeems() public {
// Stake 100 tokens as user1
_stakeAs(USER1, 100 * 10 ** tokenDecimals);

// Add Rewards
_addRewardsAs(DEPLOYER, 700 * 10 ** tokenDecimals);

_triggerNextPeriod();

// Half the distribution later...
vm.warp(block.timestamp + 3.5 days);

// ...user2 decides to stake 100 tokens!
_stakeAs(USER2, 100 * 10 ** tokenDecimals);

// Now since rewards have already been distributing...
// User1's 100 shares are worth more. (deposited at 1:1)
// So User2 staking 100 underlying should give him ~(100/4.5) shares ~22.22 shares
assertApproxEqAbs(vault.balanceOf(USER1), 100 * 10 ** vaultDecimals, maxError);
assertApproxEqAbs(vault.balanceOf(USER2), (200 * 10 ** vaultDecimals) / 9, maxError);

// Let's make sure the current accounting is correct
assertApproxEqAbs(vault.totalAssets(), 550 * 10 ** tokenDecimals, maxError);

// 1 day later..
vm.warp(block.timestamp + 1 days);

// User1 decides to redeem 50 shares
_redeemAs(USER1, 50 * 10 ** vaultDecimals);

// At this point, User1 got 100/2 + 350/2 + ~81.8/2 = ~265.9 tokens back.
assertApproxEqAbs(token.balanceOf(USER1), 265 * 10 ** tokenDecimals, 1e18);

// Let's make sure the current accounting is correct
// User1 has 50 shares left, User2 has 22.22 shares
// Without withdrawal it would be ~650, so removing ~266 from it gives us ~384
assertApproxEqAbs(vault.totalAssets(), 384 * 10 ** tokenDecimals, 1e18);

// 2.5 day later.. (the end of the distribution period)
vm.warp(block.timestamp + 2.5 days);

// Let's make sure the current accounting is correct
assertApproxEqAbs(vault.totalAssets(), 634 * 10 ** tokenDecimals, 1e18);

// Let's make sure the overall vault appreciation in correct.
assertApproxEqAbs(vault.previewRedeem(100 * 10 ** vaultDecimals), 877 * 10 ** tokenDecimals, 1e18);
}
}
Loading

0 comments on commit 4079513

Please sign in to comment.