Skip to content
This repository was archived by the owner on Aug 26, 2024. It is now read-only.

Commit 31600e4

Browse files
authored
Merge pull request #73 from ionicprotocol/add-borrow-flywheel-and-booster
Add borrow Flywheel and booster
2 parents 8eb88dd + 0160610 commit 31600e4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+4972
-90
lines changed

contracts/ionic/strategies/IonicERC4626.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ abstract contract IonicERC4626 is SafeOwnableUpgradeable, PausableUpgradeable, E
3131

3232
/* ========== INITIALIZER ========== */
3333

34-
function __MidasER4626_init(ERC20Upgradeable asset_) internal onlyInitializing {
34+
function __IonicER4626_init(ERC20Upgradeable asset_) internal onlyInitializing {
3535
__SafeOwnable_init(msg.sender);
3636
__Pausable_init();
3737
__Context_init();
3838
__ERC20_init(
39-
string(abi.encodePacked("Midas ", asset_.name(), " Vault")),
39+
string(abi.encodePacked("Ionic ", asset_.name(), " Vault")),
4040
string(abi.encodePacked("mv", asset_.symbol()))
4141
);
4242
__ERC4626_init(asset_);

contracts/ionic/strategies/MockERC4626Dynamic.sol

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ERC20 } from "solmate/tokens/ERC20.sol";
55

66
import { ERC4626 } from "solmate/mixins/ERC4626.sol";
77
import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol";
8-
import { FlywheelCore } from "flywheel-v2/FlywheelCore.sol";
8+
import { IonicFlywheelCore } from "./flywheel/IonicFlywheelCore.sol";
99

1010
/**
1111
* @title Mock ERC4626 Contract
@@ -17,7 +17,7 @@ contract MockERC4626Dynamic is ERC4626 {
1717
using FixedPointMathLib for uint256;
1818

1919
/* ========== STATE VARIABLES ========== */
20-
FlywheelCore public immutable flywheel;
20+
IonicFlywheelCore public immutable flywheel;
2121

2222
/* ========== INITIALIZER ========== */
2323

@@ -26,7 +26,7 @@ contract MockERC4626Dynamic is ERC4626 {
2626
@param _asset The ERC20 compliant token the Vault should accept.
2727
@param _flywheel Flywheel to pull in rewardsToken
2828
*/
29-
constructor(ERC20 _asset, FlywheelCore _flywheel)
29+
constructor(ERC20 _asset, IonicFlywheelCore _flywheel)
3030
ERC4626(
3131
_asset,
3232
string(abi.encodePacked("Midas ", _asset.name(), " Vault")),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
pragma solidity ^0.8.10;
3+
4+
import {ERC20} from "solmate/tokens/ERC20.sol";
5+
6+
/**
7+
@title Balance Booster Module for Flywheel
8+
@notice Flywheel is a general framework for managing token incentives.
9+
It takes reward streams to various *strategies* such as staking LP tokens and divides them among *users* of those strategies.
10+
11+
The Booster module is an optional module for virtually boosting or otherwise transforming user balances.
12+
If a booster is not configured, the strategies ERC-20 balanceOf/totalSupply will be used instead.
13+
14+
Boosting logic can be associated with referrals, vote-escrow, or other strategies.
15+
16+
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.
17+
This prevents gaming of the reward calculation function by using manipulated balances when accruing.
18+
*/
19+
interface IFlywheelBooster {
20+
/**
21+
@notice calculate the boosted supply of a strategy.
22+
@param strategy the strategy to calculate boosted supply of
23+
@return the boosted supply
24+
*/
25+
function boostedTotalSupply(ERC20 strategy) external view returns (uint256);
26+
27+
/**
28+
@notice calculate the boosted balance of a user in a given strategy.
29+
@param strategy the strategy to calculate boosted balance of
30+
@param user the user to calculate boosted balance of
31+
@return the boosted balance
32+
*/
33+
function boostedBalanceOf(ERC20 strategy, address user) external view returns (uint256);
34+
}

contracts/ionic/strategies/flywheel/IIonicFlywheel.sol

+1-5
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ interface IIonicFlywheel {
1212

1313
function flywheelPreBorrowerAction(address market, address borrower) external;
1414

15-
function flywheelPreTransferAction(
16-
address market,
17-
address src,
18-
address dst
19-
) external;
15+
function flywheelPreTransferAction(address market, address src, address dst) external;
2016

2117
function compAccrued(address user) external view returns (uint256);
2218

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
pragma solidity ^0.8.10;
3+
4+
import { ICErc20 } from "../../../compound/CTokenInterfaces.sol";
5+
6+
/**
7+
@title Balance Booster Module for Flywheel
8+
@notice Flywheel is a general framework for managing token incentives.
9+
It takes reward streams to various *strategies* such as staking LP tokens and divides them among *users* of those strategies.
10+
11+
The Booster module is an optional module for virtually boosting or otherwise transforming user balances.
12+
If a booster is not configured, the strategies ERC-20 balanceOf/totalSupply will be used instead.
13+
14+
Boosting logic can be associated with referrals, vote-escrow, or other strategies.
15+
16+
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.
17+
This prevents gaming of the reward calculation function by using manipulated balances when accruing.
18+
*/
19+
interface IIonicFlywheelBorrowBooster {
20+
/**
21+
@notice calculate the boosted supply of a strategy.
22+
@param strategy the strategy to calculate boosted supply of
23+
@return the boosted supply
24+
*/
25+
function boostedTotalSupply(ICErc20 strategy) external view returns (uint256);
26+
27+
/**
28+
@notice calculate the boosted balance of a user in a given strategy.
29+
@param strategy the strategy to calculate boosted balance of
30+
@param user the user to calculate boosted balance of
31+
@return the boosted balance
32+
*/
33+
function boostedBalanceOf(ICErc20 strategy, address user) external view returns (uint256);
34+
}

0 commit comments

Comments
 (0)