From 60443f8d98081d5855e8d318e435b9e534b98d0c Mon Sep 17 00:00:00 2001 From: Joey Santoro Date: Sat, 19 Mar 2022 19:25:44 -0700 Subject: [PATCH] natspec --- src/FlywheelCore.sol | 177 ++++++++++++++++++---------- src/interfaces/IFlywheelBooster.sol | 25 +++- src/interfaces/IFlywheelRewards.sol | 29 ++++- src/rewards/BaseFlywheelRewards.sol | 1 + 4 files changed, 163 insertions(+), 69 deletions(-) diff --git a/src/FlywheelCore.sol b/src/FlywheelCore.sol index c10e5ff..6c096a6 100644 --- a/src/FlywheelCore.sol +++ b/src/FlywheelCore.sol @@ -11,35 +11,21 @@ import {IFlywheelBooster} from "./interfaces/IFlywheelBooster.sol"; /** @title Flywheel Core Incentives Manager @notice Flywheel is a general framework for managing token incentives. - It is comprised of the Core (this contract), Rewards module, and optional Booster module. + It takes reward streams to various *strategies* such as staking LP tokens and divides them among *users* of those strategies. - Core is responsible for maintaining reward accrual through reward indexes. - It delegates the actual accrual logic to the Rewards Module. + The Core contract maintaings three important pieces of state: + * the rewards index which determines how many rewards are owed per token per strategy. User indexes track how far behind the strategy they are to lazily calculate all catch-up rewards. + * the accrued (unclaimed) rewards per user. + * references to the booster and rewards module described below. - For maximum accuracy and to avoid exploits, rewards accrual should be notified atomically through the accrue hook. + Core does not manage any tokens directly. The rewards module maintains token balances, and approves core to pull transfer them to users when they claim. + + SECURITY NOTE: For maximum accuracy and to avoid exploits, rewards accrual should be notified atomically through the accrue hook. Accrue should be called any time tokens are transferred, minted, or burned. */ contract FlywheelCore is Auth { using SafeTransferLib for ERC20; - event AddStrategy(address indexed newStrategy); - - event FlywheelRewardsUpdate(address indexed newFlywheelRewards); - - event FlywheelBoosterUpdate(address indexed newBooster); - - event AccrueRewards(ERC20 indexed cToken, address indexed owner, uint rewardsDelta, uint rewardsIndex); - - event ClaimRewards(address indexed owner, uint256 amount); - - struct RewardsState { - /// @notice The strategy's last updated index - uint224 index; - - /// @notice The timestamp the index was last updated at - uint32 lastUpdatedTimestamp; - } - /// @notice The token to reward ERC20 public immutable rewardToken; @@ -49,18 +35,6 @@ contract FlywheelCore is Auth { /// @notice optional booster module for calculating virtual balances on strategies IFlywheelBooster public flywheelBooster; - /// @notice the fixed point factor of flywheel - uint224 public constant ONE = 1e18; - - /// @notice The strategy index and last updated per strategy - mapping(ERC20 => RewardsState) public strategyState; - - /// @notice user index per strategy - mapping(ERC20 => mapping(address => uint224)) public userIndex; - - /// @notice The accrued but not yet transferred rewards for each user - mapping(address => uint256) public rewardsAccrued; - constructor( ERC20 _rewardToken, IFlywheelRewards _flywheelRewards, @@ -73,6 +47,88 @@ contract FlywheelCore is Auth { flywheelBooster = _flywheelBooster; } + /*/////////////////////////////////////////////////////////////// + ACCRUE/CLAIM LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + @notice Emitted when a user's rewards accrue to a given strategy. + @param strategy the updated rewards strategy + @param user the user of the rewards + @param rewardsDelta how many new rewards accrued to the user + @param rewardsIndex the market index for rewards per token accrued + */ + event AccrueRewards(ERC20 indexed strategy, address indexed user, uint rewardsDelta, uint rewardsIndex); + + /** + @notice Emitted when a user claims accrued rewards. + @param user the user of the rewards + @param amount the amount of rewards claimed + */ + event ClaimRewards(address indexed user, uint256 amount); + + /// @notice The accrued but not yet transferred rewards for each user + mapping(address => uint256) public rewardsAccrued; + + /** + @notice accrue rewards for a single user on a strategy + @param strategy the strategy to accrue a user's rewards on + @param user the user to be accrued + @return the cumulative amount of rewards accrued to user (including prior) + */ + function accrue(ERC20 strategy, address user) public returns (uint256) { + RewardsState memory state = strategyState[strategy]; + + if (state.index == 0) return 0; + + state = accrueStrategy(strategy, state); + return accrueUser(strategy, user, state); + } + + /** + @notice accrue rewards for a two users on a strategy + @param strategy the strategy to accrue a user's rewards on + @param user the first user to be accrued + @param user the second user to be accrued + @return the cumulative amount of rewards accrued to the first user (including prior) + @return the cumulative amount of rewards accrued to the second user (including prior) + */ + function accrue(ERC20 strategy, address user, address secondUser) public returns (uint256, uint256) { + RewardsState memory state = strategyState[strategy]; + + if (state.index == 0) return (0, 0); + + state = accrueStrategy(strategy, state); + return (accrueUser(strategy, user, state), accrueUser(strategy, secondUser, state)); + } + + /** + @notice claim rewards for a given user + @param user the user claiming rewards + @dev this function is public, and all rewards transfer to the user + */ + function claimRewards(address user) external { + uint256 accrued = rewardsAccrued[user]; + + if (accrued != 0) { + rewardsAccrued[user] = 0; + + rewardToken.safeTransferFrom(address(flywheelRewards), user, accrued); + + emit ClaimRewards(user, accrued); + } + } + + /*/////////////////////////////////////////////////////////////// + ADMIN LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + @notice Emitted when a new strategy is added to flywheel by the admin + @param newStrategy the new added strategy + */ + event AddStrategy(address indexed newStrategy); + /// @notice initialize a new strategy function addStrategyForRewards(ERC20 strategy) external requiresAuth { require(strategyState[strategy].index == 0, "strategy"); @@ -84,6 +140,12 @@ contract FlywheelCore is Auth { emit AddStrategy(address(strategy)); } + /** + @notice Emitted when the rewards module changes + @param newFlywheelRewards the new rewards module + */ + event FlywheelRewardsUpdate(address indexed newFlywheelRewards); + /// @notice swap out the flywheel rewards contract function setFlywheelRewards(IFlywheelRewards newFlywheelRewards) external requiresAuth { flywheelRewards = newFlywheelRewards; @@ -91,6 +153,12 @@ contract FlywheelCore is Auth { emit FlywheelRewardsUpdate(address(newFlywheelRewards)); } + /** + @notice Emitted when the booster module changes + @param newBooster the new booster module + */ + event FlywheelBoosterUpdate(address indexed newBooster); + /// @notice swap out the flywheel booster contract function setBooster(IFlywheelBooster newBooster) external requiresAuth { flywheelBooster = newBooster; @@ -98,38 +166,27 @@ contract FlywheelCore is Auth { emit FlywheelBoosterUpdate(address(newBooster)); } - /// @notice accrue rewards for a single user on a strategy - function accrue(ERC20 strategy, address user) public returns (uint256) { - RewardsState memory state = strategyState[strategy]; - - if (state.index == 0) return 0; + /*/////////////////////////////////////////////////////////////// + INTERNAL ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ - state = accrueStrategy(strategy, state); - return accrueUser(strategy, user, state); - } - - /// @notice accrue rewards for two users on a strategy - function accrue(ERC20 strategy, address user, address secondUser) public returns (uint256, uint256) { - RewardsState memory state = strategyState[strategy]; - - if (state.index == 0) return (0, 0); + struct RewardsState { + /// @notice The strategy's last updated index + uint224 index; - state = accrueStrategy(strategy, state); - return (accrueUser(strategy, user, state), accrueUser(strategy, secondUser, state)); + /// @notice The timestamp the index was last updated at + uint32 lastUpdatedTimestamp; } + + /// @notice the fixed point factor of flywheel + uint224 public constant ONE = 1e18; - /// @notice claim rewards for a given owner - function claimRewards(address owner) external { - uint256 accrued = rewardsAccrued[owner]; - - if (accrued != 0) { - rewardsAccrued[owner] = 0; + /// @notice The strategy index and last updated per strategy + mapping(ERC20 => RewardsState) public strategyState; - rewardToken.safeTransferFrom(address(flywheelRewards), owner, accrued); + /// @notice user index per strategy + mapping(ERC20 => mapping(address => uint224)) public userIndex; - emit ClaimRewards(owner, accrued); - } - } /// @notice accumulate global rewards on a strategy function accrueStrategy(ERC20 strategy, RewardsState memory state) private returns(RewardsState memory rewardsState) { diff --git a/src/interfaces/IFlywheelBooster.sol b/src/interfaces/IFlywheelBooster.sol index 9ea97a9..c1aa6e5 100644 --- a/src/interfaces/IFlywheelBooster.sol +++ b/src/interfaces/IFlywheelBooster.sol @@ -5,12 +5,31 @@ import {ERC20} from "solmate/tokens/ERC20.sol"; /** @title Balance Booster Module for Flywheel - @notice An optional module for virtually boosting user balances. This allows a Flywheel Core to plug into some balance boosting logic. + @notice Flywheel is a general framework for managing token incentives. + It takes reward streams to various *strategies* such as staking LP tokens and divides them among *users* of those strategies. - Boosting logic can be associated with referrals, vote-escrow, or other strategies. It can even be used to model exotic strategies like borrowing. - */ + The Booster module is an optional module for virtually boosting or otherwise transforming user balances. + If a booster is not configured, the strategies ERC-20 balanceOf/totalSupply will be used instead. + + Boosting logic can be associated with referrals, vote-escrow, or other strategies. + + SECURITY NOTE: similar to how Core needs to be notified any time the strategy user composition changes, the booster would need to be notified of any conditions which change the boosted balances atomically. + This prevents gaming of the reward calculation function by using manipulated balances when accruing. +*/ interface IFlywheelBooster { + + /** + @notice calculate the boosted supply of a strategy. + @param strategy the strategy to calculate boosted supply of + @return the boosted supply + */ function boostedTotalSupply(ERC20 strategy) external view returns(uint256); + /** + @notice calculate the boosted balance of a user in a given strategy. + @param strategy the strategy to calculate boosted balance of + @param user the user to calculate boosted balance of + @return the boosted balance + */ function boostedBalanceOf(ERC20 strategy, address user) external view returns(uint256); } diff --git a/src/interfaces/IFlywheelRewards.sol b/src/interfaces/IFlywheelRewards.sol index 46f1b9e..e13996e 100644 --- a/src/interfaces/IFlywheelRewards.sol +++ b/src/interfaces/IFlywheelRewards.sol @@ -6,18 +6,35 @@ import {FlywheelCore} from "../FlywheelCore.sol"; /** @title Rewards Module for Flywheel - @notice The rewards module is a minimal interface for determining the quantity of rewards accrued to a flywheel strategy. + @notice Flywheel is a general framework for managing token incentives. + It takes reward streams to various *strategies* such as staking LP tokens and divides them among *users* of those strategies. - Different module strategies include: - * a static reward rate per second - * a decaying reward rate - * a dynamic just-in-time reward stream - * liquid governance reward delegation + The Rewards module is responsible for: + * determining the ongoing reward amounts to entire strategies (core handles the logic for dividing among users) + * actually holding rewards that are yet to be claimed + + The reward stream can follow arbitrary logic as long as the amount of rewards passed to flywheel core has been sent to this contract. + + Different module strategies include: + * a static reward rate per second + * a decaying reward rate + * a dynamic just-in-time reward stream + * liquid governance reward delegation (Curve Gauge style) + + SECURITY NOTE: The rewards strategy should be smooth and continuous, to prevent gaming the reward distribution by frontrunning. */ interface IFlywheelRewards { + /** + @notice calculate the rewards amount accrued to a strategy since the last update. + @param strategy the strategy to accrue rewards for. + @param lastUpdatedTimestamp the last time rewards were accrued for the strategy. + @return rewards the amount of rewards accrued to the market + */ function getAccruedRewards(ERC20 strategy, uint32 lastUpdatedTimestamp) external returns (uint256 rewards); + /// @notice return the flywheel core address function flywheel() external view returns(FlywheelCore); + /// @notice return the reward token associated with flywheel core. function rewardToken() external view returns(ERC20); } diff --git a/src/rewards/BaseFlywheelRewards.sol b/src/rewards/BaseFlywheelRewards.sol index e491737..1819e34 100644 --- a/src/rewards/BaseFlywheelRewards.sol +++ b/src/rewards/BaseFlywheelRewards.sol @@ -8,6 +8,7 @@ import {FlywheelCore} from "../FlywheelCore.sol"; /** @title Flywheel Reward Module @notice Determines how many rewards accrue to each strategy globally over a given time period. + @dev approves the flywheel core for the reward token to allow balances to be managed by the module but claimed from core. */ abstract contract BaseFlywheelRewards is IFlywheelRewards { using SafeTransferLib for ERC20;