|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-only |
| 2 | +pragma solidity ^0.8.10; |
| 3 | + |
| 4 | +import {ERC20} from "solmate/tokens/ERC20.sol"; |
| 5 | +import {Auth, Authority} from "solmate/auth/Auth.sol"; |
| 6 | +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; |
| 7 | +import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; |
| 8 | + |
| 9 | +import {IFlywheelRewards} from "./rewards/IFlywheelRewards.sol"; |
| 10 | +import {IFlywheelBooster} from "./IFlywheelBooster.sol"; |
| 11 | + |
| 12 | +/** |
| 13 | + @title Flywheel Core Incentives Manager |
| 14 | + @notice Flywheel is a general framework for managing token incentives. |
| 15 | + It takes reward streams to various *strategies* such as staking LP tokens and divides them among *users* of those strategies. |
| 16 | +
|
| 17 | + The Core contract maintaings three important pieces of state: |
| 18 | + * 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. |
| 19 | + * the accrued (unclaimed) rewards per user. |
| 20 | + * references to the booster and rewards module described below. |
| 21 | +
|
| 22 | + 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. |
| 23 | +
|
| 24 | + SECURITY NOTE: For maximum accuracy and to avoid exploits, rewards accrual should be notified atomically through the accrue hook. |
| 25 | + Accrue should be called any time tokens are transferred, minted, or burned. |
| 26 | + */ |
| 27 | +contract FlywheelCore is Auth { |
| 28 | + using SafeTransferLib for ERC20; |
| 29 | + using SafeCastLib for uint256; |
| 30 | + |
| 31 | + /// @notice The token to reward |
| 32 | + ERC20 public immutable rewardToken; |
| 33 | + |
| 34 | + /// @notice append-only list of strategies added |
| 35 | + ERC20[] public allStrategies; |
| 36 | + |
| 37 | + /// @notice the rewards contract for managing streams |
| 38 | + IFlywheelRewards public flywheelRewards; |
| 39 | + |
| 40 | + /// @notice optional booster module for calculating virtual balances on strategies |
| 41 | + IFlywheelBooster public flywheelBooster; |
| 42 | + |
| 43 | + constructor( |
| 44 | + ERC20 _rewardToken, |
| 45 | + IFlywheelRewards _flywheelRewards, |
| 46 | + IFlywheelBooster _flywheelBooster, |
| 47 | + address _owner, |
| 48 | + Authority _authority |
| 49 | + ) Auth(_owner, _authority) { |
| 50 | + rewardToken = _rewardToken; |
| 51 | + flywheelRewards = _flywheelRewards; |
| 52 | + flywheelBooster = _flywheelBooster; |
| 53 | + } |
| 54 | + |
| 55 | + /*/////////////////////////////////////////////////////////////// |
| 56 | + ACCRUE/CLAIM LOGIC |
| 57 | + //////////////////////////////////////////////////////////////*/ |
| 58 | + |
| 59 | + /** |
| 60 | + @notice Emitted when a user's rewards accrue to a given strategy. |
| 61 | + @param strategy the updated rewards strategy |
| 62 | + @param user the user of the rewards |
| 63 | + @param rewardsDelta how many new rewards accrued to the user |
| 64 | + @param rewardsIndex the market index for rewards per token accrued |
| 65 | + */ |
| 66 | + event AccrueRewards(ERC20 indexed strategy, address indexed user, uint256 rewardsDelta, uint256 rewardsIndex); |
| 67 | + |
| 68 | + /** |
| 69 | + @notice Emitted when a user claims accrued rewards. |
| 70 | + @param user the user of the rewards |
| 71 | + @param amount the amount of rewards claimed |
| 72 | + */ |
| 73 | + event ClaimRewards(address indexed user, uint256 amount); |
| 74 | + |
| 75 | + /// @notice The accrued but not yet transferred rewards for each user |
| 76 | + mapping(address => uint256) public rewardsAccrued; |
| 77 | + |
| 78 | + /** |
| 79 | + @notice accrue rewards for a single user on a strategy |
| 80 | + @param strategy the strategy to accrue a user's rewards on |
| 81 | + @param user the user to be accrued |
| 82 | + @return the cumulative amount of rewards accrued to user (including prior) |
| 83 | + */ |
| 84 | + function accrue(ERC20 strategy, address user) public returns (uint256) { |
| 85 | + RewardsState memory state = strategyState[strategy]; |
| 86 | + |
| 87 | + if (state.index == 0) return 0; |
| 88 | + |
| 89 | + state = accrueStrategy(strategy, state); |
| 90 | + return accrueUser(strategy, user, state); |
| 91 | + } |
| 92 | + |
| 93 | + /** |
| 94 | + @notice accrue rewards for a two users on a strategy |
| 95 | + @param strategy the strategy to accrue a user's rewards on |
| 96 | + @param user the first user to be accrued |
| 97 | + @param user the second user to be accrued |
| 98 | + @return the cumulative amount of rewards accrued to the first user (including prior) |
| 99 | + @return the cumulative amount of rewards accrued to the second user (including prior) |
| 100 | + */ |
| 101 | + function accrue( |
| 102 | + ERC20 strategy, |
| 103 | + address user, |
| 104 | + address secondUser |
| 105 | + ) public returns (uint256, uint256) { |
| 106 | + RewardsState memory state = strategyState[strategy]; |
| 107 | + |
| 108 | + if (state.index == 0) return (0, 0); |
| 109 | + |
| 110 | + state = accrueStrategy(strategy, state); |
| 111 | + return (accrueUser(strategy, user, state), accrueUser(strategy, secondUser, state)); |
| 112 | + } |
| 113 | + |
| 114 | + /** |
| 115 | + @notice claim rewards for a given user |
| 116 | + @param user the user claiming rewards |
| 117 | + @dev this function is public, and all rewards transfer to the user |
| 118 | + */ |
| 119 | + function claimRewards(address user) external { |
| 120 | + uint256 accrued = rewardsAccrued[user]; |
| 121 | + |
| 122 | + if (accrued != 0) { |
| 123 | + rewardsAccrued[user] = 0; |
| 124 | + |
| 125 | + rewardToken.safeTransferFrom(address(flywheelRewards), user, accrued); |
| 126 | + |
| 127 | + emit ClaimRewards(user, accrued); |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + /*/////////////////////////////////////////////////////////////// |
| 132 | + ADMIN LOGIC |
| 133 | + //////////////////////////////////////////////////////////////*/ |
| 134 | + |
| 135 | + /** |
| 136 | + @notice Emitted when a new strategy is added to flywheel by the admin |
| 137 | + @param newStrategy the new added strategy |
| 138 | + */ |
| 139 | + event AddStrategy(address indexed newStrategy); |
| 140 | + |
| 141 | + /// @notice initialize a new strategy |
| 142 | + function addStrategyForRewards(ERC20 strategy) external requiresAuth { |
| 143 | + _addStrategyForRewards(strategy); |
| 144 | + } |
| 145 | + |
| 146 | + function _addStrategyForRewards(ERC20 strategy) internal { |
| 147 | + require(strategyState[strategy].index == 0, "strategy"); |
| 148 | + strategyState[strategy] = RewardsState({index: ONE, lastUpdatedTimestamp: block.timestamp.safeCastTo32()}); |
| 149 | + |
| 150 | + allStrategies.push(strategy); |
| 151 | + emit AddStrategy(address(strategy)); |
| 152 | + } |
| 153 | + |
| 154 | + function getAllStrategies() external view returns (ERC20[] memory) { |
| 155 | + return allStrategies; |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + @notice Emitted when the rewards module changes |
| 160 | + @param newFlywheelRewards the new rewards module |
| 161 | + */ |
| 162 | + event FlywheelRewardsUpdate(address indexed newFlywheelRewards); |
| 163 | + |
| 164 | + /// @notice swap out the flywheel rewards contract |
| 165 | + function setFlywheelRewards(IFlywheelRewards newFlywheelRewards) external requiresAuth { |
| 166 | + uint256 oldRewardBalance = rewardToken.balanceOf(address(flywheelRewards)); |
| 167 | + if (oldRewardBalance > 0) { |
| 168 | + rewardToken.safeTransferFrom(address(flywheelRewards), address(newFlywheelRewards), oldRewardBalance); |
| 169 | + } |
| 170 | + |
| 171 | + flywheelRewards = newFlywheelRewards; |
| 172 | + |
| 173 | + emit FlywheelRewardsUpdate(address(newFlywheelRewards)); |
| 174 | + } |
| 175 | + |
| 176 | + /** |
| 177 | + @notice Emitted when the booster module changes |
| 178 | + @param newBooster the new booster module |
| 179 | + */ |
| 180 | + event FlywheelBoosterUpdate(address indexed newBooster); |
| 181 | + |
| 182 | + /// @notice swap out the flywheel booster contract |
| 183 | + function setBooster(IFlywheelBooster newBooster) external requiresAuth { |
| 184 | + flywheelBooster = newBooster; |
| 185 | + |
| 186 | + emit FlywheelBoosterUpdate(address(newBooster)); |
| 187 | + } |
| 188 | + |
| 189 | + /*/////////////////////////////////////////////////////////////// |
| 190 | + INTERNAL ACCOUNTING LOGIC |
| 191 | + //////////////////////////////////////////////////////////////*/ |
| 192 | + |
| 193 | + struct RewardsState { |
| 194 | + /// @notice The strategy's last updated index |
| 195 | + uint224 index; |
| 196 | + /// @notice The timestamp the index was last updated at |
| 197 | + uint32 lastUpdatedTimestamp; |
| 198 | + } |
| 199 | + |
| 200 | + /// @notice the fixed point factor of flywheel |
| 201 | + uint224 public constant ONE = 1e18; |
| 202 | + |
| 203 | + /// @notice The strategy index and last updated per strategy |
| 204 | + mapping(ERC20 => RewardsState) public strategyState; |
| 205 | + |
| 206 | + /// @notice user index per strategy |
| 207 | + mapping(ERC20 => mapping(address => uint224)) public userIndex; |
| 208 | + |
| 209 | + /// @notice accumulate global rewards on a strategy |
| 210 | + function accrueStrategy(ERC20 strategy, RewardsState memory state) |
| 211 | + private |
| 212 | + returns (RewardsState memory rewardsState) |
| 213 | + { |
| 214 | + // calculate accrued rewards through module |
| 215 | + uint256 strategyRewardsAccrued = flywheelRewards.getAccruedRewards(strategy, state.lastUpdatedTimestamp); |
| 216 | + |
| 217 | + rewardsState = state; |
| 218 | + if (strategyRewardsAccrued > 0) { |
| 219 | + // use the booster or token supply to calculate reward index denominator |
| 220 | + uint256 supplyTokens = address(flywheelBooster) != address(0) |
| 221 | + ? flywheelBooster.boostedTotalSupply(strategy) |
| 222 | + : strategy.totalSupply(); |
| 223 | + |
| 224 | + uint224 deltaIndex; |
| 225 | + |
| 226 | + if (supplyTokens != 0) deltaIndex = ((strategyRewardsAccrued * ONE) / supplyTokens).safeCastTo224(); |
| 227 | + |
| 228 | + // accumulate rewards per token onto the index, multiplied by fixed-point factor |
| 229 | + rewardsState = RewardsState({ |
| 230 | + index: state.index + deltaIndex, |
| 231 | + lastUpdatedTimestamp: block.timestamp.safeCastTo32() |
| 232 | + }); |
| 233 | + strategyState[strategy] = rewardsState; |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + /// @notice accumulate rewards on a strategy for a specific user |
| 238 | + function accrueUser( |
| 239 | + ERC20 strategy, |
| 240 | + address user, |
| 241 | + RewardsState memory state |
| 242 | + ) private returns (uint256) { |
| 243 | + // load indices |
| 244 | + uint224 strategyIndex = state.index; |
| 245 | + uint224 supplierIndex = userIndex[strategy][user]; |
| 246 | + |
| 247 | + // sync user index to global |
| 248 | + userIndex[strategy][user] = strategyIndex; |
| 249 | + |
| 250 | + // if user hasn't yet accrued rewards, grant them interest from the strategy beginning if they have a balance |
| 251 | + // zero balances will have no effect other than syncing to global index |
| 252 | + if (supplierIndex == 0) { |
| 253 | + supplierIndex = ONE; |
| 254 | + } |
| 255 | + |
| 256 | + uint224 deltaIndex = strategyIndex - supplierIndex; |
| 257 | + // use the booster or token balance to calculate reward balance multiplier |
| 258 | + uint256 supplierTokens = address(flywheelBooster) != address(0) |
| 259 | + ? flywheelBooster.boostedBalanceOf(strategy, user) |
| 260 | + : strategy.balanceOf(user); |
| 261 | + |
| 262 | + // accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed |
| 263 | + uint256 supplierDelta = (supplierTokens * deltaIndex) / ONE; |
| 264 | + uint256 supplierAccrued = rewardsAccrued[user] + supplierDelta; |
| 265 | + |
| 266 | + rewardsAccrued[user] = supplierAccrued; |
| 267 | + |
| 268 | + emit AccrueRewards(strategy, user, supplierDelta, strategyIndex); |
| 269 | + |
| 270 | + return supplierAccrued; |
| 271 | + } |
| 272 | +} |
0 commit comments