generated from transmissions11/dapptools-template
-
Notifications
You must be signed in to change notification settings - Fork 27
/
FlywheelGaugeRewards.sol
276 lines (212 loc) · 10.8 KB
/
FlywheelGaugeRewards.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.10;
import {Auth, Authority} from "solmate/auth/Auth.sol";
import {SafeCastLib} from "solmate/utils/SafeCastLib.sol";
import "./BaseFlywheelRewards.sol";
import {ERC20Gauges} from "../token/ERC20Gauges.sol";
/// @notice a contract which streams reward tokens to the FlywheelRewards module
interface IRewardsStream {
/// @notice read and transfer reward token chunk to FlywheelRewards module
function getRewards() external returns (uint256);
}
/**
@title Flywheel Gauge Reward Stream
@notice Distributes rewards from a stream based on gauge weights
The contract assumes an arbitrary stream of rewards `S` of rewardToken. It chunks the rewards into cycles of length `l`.
The allocation function for each cycle A(g, S) proportions the stream to each gauge such that SUM(A(g, S)) over all gauges <= S.
NOTE it should be approximately S, but may be less due to truncation.
Rewards are accumulated every time a new rewards cycle begins, and all prior rewards are cached in the previous cycle.
When the Flywheel Core requests accrued rewards for a specific gauge:
1. All prior rewards before this cycle are distributed
2. Rewards for the current cycle are distributed proportionally to the remaining time in the cycle.
If `e` is the cycle end, `t` is the min of e and current timestamp, and `p` is the prior updated time:
For `A` accrued rewards over the cycle, distribute `min(A * (t-p)/(e-p), A)`.
*/
contract FlywheelGaugeRewards is Auth, BaseFlywheelRewards {
using SafeTransferLib for ERC20;
using SafeCastLib for uint256;
/// @notice thrown when trying to queue a new cycle during an old one.
error CycleError();
/// @notice thrown when trying to queue with 0 gauges
error EmptyGaugesError();
/// @notice emitted when a cycle has completely queued and started
event CycleStart(uint32 indexed cycleStart, uint256 rewardAmount);
/// @notice emitted when a single gauge is queued. May be emitted before the cycle starts if the queue is done via pagination.
event QueueRewards(address indexed gauge, uint32 indexed cycleStart, uint256 rewardAmount);
/// @notice the start of the current cycle
uint32 public gaugeCycle;
/// @notice the length of a rewards cycle
uint32 public immutable gaugeCycleLength;
/// @notice the start of the next cycle being partially queued
uint32 internal nextCycle;
// rewards that made it into a partial queue but didn't get completed
uint112 internal nextCycleQueuedRewards;
// the offset during pagination of the queue
uint32 internal paginationOffset;
/// @notice rewards queued from prior and current cycles
struct QueuedRewards {
uint112 priorCycleRewards;
uint112 cycleRewards;
uint32 storedCycle;
}
/// @notice mapping from gauges to queued rewards
mapping(ERC20 => QueuedRewards) public gaugeQueuedRewards;
/// @notice the gauge token for determining gauge allocations of the rewards stream
ERC20Gauges public immutable gaugeToken;
/// @notice contract to pull reward tokens from
IRewardsStream public rewardsStream;
constructor(
FlywheelCore _flywheel,
address _owner,
Authority _authority,
ERC20Gauges _gaugeToken,
IRewardsStream _rewardsStream
) BaseFlywheelRewards(_flywheel) Auth(_owner, _authority) {
gaugeCycleLength = _gaugeToken.gaugeCycleLength();
// seed initial gaugeCycle
gaugeCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;
gaugeToken = _gaugeToken;
rewardsStream = _rewardsStream;
}
/**
@notice Iterates over all live gauges and queues up the rewards for the cycle
@return totalQueuedForCycle the max amount of rewards to be distributed over the cycle
*/
function queueRewardsForCycle() external requiresAuth returns (uint256 totalQueuedForCycle) {
// next cycle is always the next even divisor of the cycle length above current block timestamp.
uint32 currentCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;
uint32 lastCycle = gaugeCycle;
// ensure new cycle has begun
if (currentCycle <= lastCycle) revert CycleError();
gaugeCycle = currentCycle;
// queue the rewards stream and sanity check the tokens were received
uint256 balanceBefore = rewardToken.balanceOf(address(this));
totalQueuedForCycle = rewardsStream.getRewards();
require(rewardToken.balanceOf(address(this)) - balanceBefore >= totalQueuedForCycle);
// include uncompleted cycle
totalQueuedForCycle += nextCycleQueuedRewards;
// iterate over all gauges and update the rewards allocations
address[] memory gauges = gaugeToken.gauges();
_queueRewards(gauges, currentCycle, lastCycle, totalQueuedForCycle);
nextCycleQueuedRewards = 0;
paginationOffset = 0;
emit CycleStart(currentCycle, totalQueuedForCycle);
}
/**
@notice Iterates over all live gauges and queues up the rewards for the cycle
*/
function queueRewardsForCyclePaginated(uint256 numRewards) external requiresAuth {
// next cycle is always the next even divisor of the cycle length above current block timestamp.
uint32 currentCycle = (block.timestamp.safeCastTo32() / gaugeCycleLength) * gaugeCycleLength;
uint32 lastCycle = gaugeCycle;
// ensure new cycle has begun
if (currentCycle <= lastCycle) revert CycleError();
if (currentCycle > nextCycle) {
nextCycle = currentCycle;
paginationOffset = 0;
}
uint32 offset = paginationOffset;
// important to only calculate the reward amount once to prevent each page from having a different reward amount
if (offset == 0) {
// queue the rewards stream and sanity check the tokens were received
uint256 balanceBefore = rewardToken.balanceOf(address(this));
uint256 newRewards = rewardsStream.getRewards();
require(rewardToken.balanceOf(address(this)) - balanceBefore >= newRewards);
require(newRewards <= type(uint112).max); // safe cast
nextCycleQueuedRewards += uint112(newRewards); // in case a previous incomplete cycle had rewards, add on
}
uint112 queued = nextCycleQueuedRewards;
uint256 remaining = gaugeToken.numGauges() - offset;
// Important to do non-strict inequality to include the case where the numRewards is just enough to complete the cycle
if (remaining <= numRewards) {
numRewards = remaining;
gaugeCycle = currentCycle;
nextCycleQueuedRewards = 0;
paginationOffset = 0;
emit CycleStart(currentCycle, queued);
} else {
paginationOffset = offset + numRewards.safeCastTo32();
}
// iterate over all gauges and update the rewards allocations
address[] memory gauges = gaugeToken.gauges(offset, numRewards);
_queueRewards(gauges, currentCycle, lastCycle, queued);
}
function _queueRewards(
address[] memory gauges,
uint32 currentCycle,
uint32 lastCycle,
uint256 totalQueuedForCycle
) internal {
uint256 size = gauges.length;
if (size == 0) revert EmptyGaugesError();
for (uint256 i = 0; i < size; i++) {
ERC20 gauge = ERC20(gauges[i]);
QueuedRewards memory queuedRewards = gaugeQueuedRewards[gauge];
// Cycle queue already started
require(queuedRewards.storedCycle < currentCycle);
assert(queuedRewards.storedCycle == 0 || queuedRewards.storedCycle >= lastCycle);
uint112 completedRewards = queuedRewards.storedCycle == lastCycle ? queuedRewards.cycleRewards : 0;
uint256 nextRewards = gaugeToken.calculateGaugeAllocation(address(gauge), totalQueuedForCycle);
require(nextRewards <= type(uint112).max); // safe cast
gaugeQueuedRewards[gauge] = QueuedRewards({
priorCycleRewards: queuedRewards.priorCycleRewards + completedRewards,
cycleRewards: uint112(nextRewards),
storedCycle: currentCycle
});
emit QueueRewards(address(gauge), currentCycle, nextRewards);
}
}
/**
@notice calculate and transfer accrued rewards to flywheel core
@param gauge the gauge to accrue rewards for
@param lastUpdatedTimestamp the last updated time for gauge
@return accruedRewards the amount of reward tokens accrued.
*/
function getAccruedRewards(ERC20 gauge, uint32 lastUpdatedTimestamp)
external
override
onlyFlywheel
returns (uint256 accruedRewards)
{
QueuedRewards memory queuedRewards = gaugeQueuedRewards[gauge];
uint32 cycle = gaugeCycle;
bool incompleteCycle = queuedRewards.storedCycle > cycle;
// no rewards
if (queuedRewards.priorCycleRewards == 0 && (queuedRewards.cycleRewards == 0 || incompleteCycle)) {
return 0;
}
// if stored cycle != 0 it must be >= the last queued cycle
assert(queuedRewards.storedCycle >= cycle);
uint32 cycleEnd = cycle + gaugeCycleLength;
// always accrue prior rewards
accruedRewards = queuedRewards.priorCycleRewards;
uint112 cycleRewardsNext = queuedRewards.cycleRewards;
if (incompleteCycle) {
// If current cycle queue incomplete, do nothing to current cycle rewards or accrued
} else if (block.timestamp >= cycleEnd) {
// If cycle ended, accrue all rewards
accruedRewards += cycleRewardsNext;
cycleRewardsNext = 0;
} else {
uint32 beginning = lastUpdatedTimestamp > cycle ? lastUpdatedTimestamp : cycle;
// otherwise, return proportion of remaining rewards in cycle
uint32 elapsed = block.timestamp.safeCastTo32() - beginning;
uint32 remaining = cycleEnd - beginning;
// Casted up to avoid intermediate overflow
// cannot end in an overflow of uint112 because elapsed <= remaining and cycleRewards <= uint112.max
uint256 currentAccrued = (uint256(queuedRewards.cycleRewards) * elapsed) / remaining;
// add proportion of current cycle to accrued rewards
accruedRewards += currentAccrued;
cycleRewardsNext -= uint112(currentAccrued);
}
gaugeQueuedRewards[gauge] = QueuedRewards({
priorCycleRewards: 0,
cycleRewards: cycleRewardsNext,
storedCycle: queuedRewards.storedCycle
});
}
/// @notice set the rewards stream contract
function setRewardsStream(IRewardsStream newRewardsStream) external requiresAuth {
rewardsStream = newRewardsStream;
}
}