-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathStakingRewardDistributor.sol
More file actions
624 lines (524 loc) · 25.7 KB
/
StakingRewardDistributor.sol
File metadata and controls
624 lines (524 loc) · 25.7 KB
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { WalletConnectConfig } from "./WalletConnectConfig.sol";
import { StakeWeight } from "./StakeWeight.sol";
import { Math128 } from "./library/Math128.sol";
import { L2WCT } from "./L2WCT.sol";
import { Pauser } from "./Pauser.sol";
/**
* @title StakingRewardDistributor
* @notice This contract manages the distribution of staking rewards for the WalletConnect token.
* @dev Implements a weekly reward distribution system based on user stake weights (inspired by Curve's FeeDistributor
* and PancakeSwap's RevenueSharingPool)
* @author WalletConnect
*/
contract StakingRewardDistributor is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;
/// @notice Emitted when the contract is killed and emergency return is triggered
event Killed();
/// @notice Emitted when a token checkpoint is created
event TokenCheckpointed(uint256 timestamp, uint256 tokens);
/// @notice Emitted when rewards are claimed
event RewardsClaimed(
address indexed user, address indexed recipient, uint256 amount, uint256 claimEpoch, uint256 maxEpoch
);
/// @notice Emitted when a user updates their recipient address
/// @param user The user who updated their recipient
/// @param oldRecipient The previous recipient address
/// @param newRecipient The new recipient address
event RecipientUpdated(address indexed user, address indexed oldRecipient, address indexed newRecipient);
/// @notice Emitted when rewards are injected into the system
/// @param timestamp The timestamp for the reward injection
/// @param amount The amount of rewards injected
event RewardInjected(uint256 indexed timestamp, uint256 amount);
/// @notice Emitted when total supply checkpoint is updated
/// @param timestamp The timestamp of the checkpoint
event TotalSupplyCheckpointed(uint256 indexed timestamp);
/// @notice Thrown when attempting to interact with a killed contract
error ContractKilled();
/// @notice Thrown when an invalid configuration is provided
error InvalidConfig();
/// @notice Thrown when an invalid emergency return address is provided
error InvalidEmergencyReturn();
/// @notice Thrown when an invalid admin address is provided
error InvalidAdmin();
/// @notice Thrown when the contract is paused
error Paused();
// Roles for access control
bytes32 public constant REWARD_MANAGER_ROLE = keccak256("REWARD_MANAGER_ROLE");
// Version for tracking upgrades
uint256 public constant VERSION = 2;
// Maximum iterations for reward distribution loops (approx 1 year)
uint256 private constant MAX_REWARD_ITERATIONS = 52;
// Maximum iterations for binary search
uint256 private constant MAX_BINARY_SEARCH_ITERATIONS = 128;
/// @notice Thrown when a timestamp is before startWeekCursor
error InvalidTimestamp();
/// @notice Thrown when a user is not authorized to claim rewards for another user
error UnauthorizedClaimer();
/// @notice Thrown when transfer restrictions are still enabled
error TransferRestrictionsEnabled();
/// @notice The starting week cursor for the distribution
uint256 public startWeekCursor;
/// @notice The current week cursor for the distribution
uint256 public weekCursor;
/// @notice Mapping of user addresses to their individual week cursors
mapping(address account => uint256 weekCursor) public weekCursorOf;
/// @notice Mapping of user addresses to their current epoch
mapping(address account => uint256 userEpoch) public userEpochOf;
/// @notice Timestamp of the last token distribution
uint256 public lastTokenTimestamp;
/// @notice Mapping of weeks to the number of tokens distributed in that week
mapping(uint256 week => uint256 tokens) public tokensPerWeek;
/// @notice The WalletConnectConfig contract instance
WalletConnectConfig public config;
/// @notice The balance of tokens at the last distribution
uint256 public lastTokenBalance;
/// @notice The total number of tokens distributed so far
uint256 public totalDistributed;
/// @notice Mapping of weeks to the total StakeWeight supply at that week's bounds
mapping(uint256 => uint256) public totalSupplyAt;
/// @notice Mapping of user addresses to their designated recipient addresses for claims
/// @dev User can set recipient address for claim
mapping(address => address) public recipient;
/// @notice Flag indicating whether the contract has been killed
bool public isKilled;
/// @notice Address to receive tokens when the contract is emergency stopped
address public emergencyReturn;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/// @notice Initializes the contract
/// @notice Initialization parameters
struct Init {
/// @param admin Address of the admin
address admin;
/// @param startTime Start time for the distribution
uint256 startTime;
/// @param emergencyReturn Address for emergency return
address emergencyReturn;
/// @param config Address of the WalletConnectConfig contract
address config;
}
/// @notice Initializes the contract
/// @param init Initialization parameters
function initialize(Init memory init) external initializer {
__AccessControl_init();
__ReentrancyGuard_init();
if (init.admin == address(0)) revert InvalidAdmin();
if (init.config == address(0)) revert InvalidConfig();
if (init.emergencyReturn == address(0)) revert InvalidEmergencyReturn();
config = WalletConnectConfig(init.config);
uint256 startTimeFloorWeek = _timestampToFloorWeek(init.startTime);
startWeekCursor = startTimeFloorWeek;
lastTokenTimestamp = startTimeFloorWeek;
weekCursor = startTimeFloorWeek;
emergencyReturn = init.emergencyReturn;
// Grant roles to the admin
_grantRole(DEFAULT_ADMIN_ROLE, init.admin);
_grantRole(REWARD_MANAGER_ROLE, init.admin);
}
/// @notice Migrate from Ownable to AccessControl - for upgrading live contracts
/// @dev MUST be called atomically with upgrade via upgradeToAndCall from ProxyAdmin
/// @dev Hardcodes role assignments to prevent front-running during migration:
/// - DEFAULT_ADMIN_ROLE: Optimism Admin Timelock (controls all roles + emergency functions)
/// - REWARD_MANAGER_ROLE: Treasury MultiSig (manages reward operations)
function migrateToAccessControl() external reinitializer(2) {
// Hardcode addresses for security - prevents front-running attacks
address OPTIMISM_ADMIN_TIMELOCK = 0x61cc6aF18C351351148815c5F4813A16DEe7A7E4;
address TREASURY_MULTISIG = 0xa86Ca428512D0A18828898d2e656E9eb1b6bA6E7;
__AccessControl_init();
// Grant admin role to timelock (can grant/revoke all roles, call kill())
_grantRole(DEFAULT_ADMIN_ROLE, OPTIMISM_ADMIN_TIMELOCK);
// Grant reward manager role to treasury (can call injectReward functions)
_grantRole(REWARD_MANAGER_ROLE, TREASURY_MULTISIG);
}
modifier onlyLive() {
if (isKilled) revert ContractKilled();
_checkNotPaused();
_;
}
/// @dev Internal function to check if contract is paused
function _checkNotPaused() internal view {
if (Pauser(config.getPauser()).isStakingRewardDistributorPaused()) {
revert Paused();
}
}
/// @notice Get StakeWeight balance of "user" at "timestamp"
/// @param user The user address
/// @param timestamp The timestamp to get user's balance
function balanceOfAt(address user, uint256 timestamp) external view returns (uint256) {
StakeWeight stakeWeight = StakeWeight(config.getStakeWeight());
uint256 maxUserEpoch = stakeWeight.userPointEpoch(user);
if (maxUserEpoch == 0) {
return 0;
}
uint256 epoch = _findTimestampUserEpoch(user, timestamp, maxUserEpoch);
// Check if user was permanent at this epoch
uint256 perm = stakeWeight.userPermanentAt(user, epoch);
if (perm > 0) {
return perm;
}
// User had a decaying lock, calculate with slope
StakeWeight.Point memory point = stakeWeight.userPointHistory(user, epoch);
int128 bias = point.bias - point.slope * SafeCast.toInt128(int256(timestamp - point.timestamp));
if (bias < 0) {
return 0;
}
return SafeCast.toUint256(bias);
}
/// @notice Record token distribution checkpoint
/**
* @dev Checkpoints and distributes tokens across weeks since the last checkpoint.
*
* Key points:
* - Distribution starts from lastTokenTimestamp and goes up to current block.timestamp
* - Tokens are distributed proportionally across all affected weeks
* - Handles partial weeks at the start and end of the distribution period
* - Total distributed always matches input amount, regardless of time elapsed (considering rounding errors)
*
* Key variables:
* timeCursor: Tracks current position in time during week iterations
* deltaSinceLastTimestamp: Total time since last checkpoint, used for proportions
* thisWeekCursor: Start of the current week being processed
*/
function _checkpointToken() internal {
// Find out how many tokens to be distributed
uint256 rewardTokenBalance = IERC20(config.getL2wct()).balanceOf(address(this));
uint256 toDistribute = rewardTokenBalance - lastTokenBalance;
lastTokenBalance = rewardTokenBalance;
totalDistributed += toDistribute;
// Prepare and update time-related variables
// 1. Setup timeCursor to be the "lastTokenTimestamp"
// 2. Find out how long from previous checkpoint
// 3. Setup iterable cursor
// 4. Update lastTokenTimestamp to be block.timestamp
uint256 timeCursor = lastTokenTimestamp;
uint256 deltaSinceLastTimestamp = block.timestamp - timeCursor;
uint256 thisWeekCursor = _timestampToFloorWeek(timeCursor);
uint256 nextWeekCursor = 0;
lastTokenTimestamp = block.timestamp;
// Iterate through weeks to filled out missing tokensPerWeek (if any)
for (uint256 i = 0; i < MAX_REWARD_ITERATIONS; i++) {
nextWeekCursor = thisWeekCursor + 1 weeks;
// if block.timestamp < nextWeekCursor, means nextWeekCursor goes
// beyond the actual block.timestamp, hence it is the last iteration
// to fill out tokensPerWeek
if (block.timestamp < nextWeekCursor) {
if (deltaSinceLastTimestamp == 0 && block.timestamp == timeCursor) {
tokensPerWeek[thisWeekCursor] = tokensPerWeek[thisWeekCursor] + toDistribute;
} else {
tokensPerWeek[thisWeekCursor] = tokensPerWeek[thisWeekCursor]
+ ((toDistribute * (block.timestamp - timeCursor)) / deltaSinceLastTimestamp);
}
break;
} else {
tokensPerWeek[thisWeekCursor] = tokensPerWeek[thisWeekCursor]
+ ((toDistribute * (nextWeekCursor - timeCursor)) / deltaSinceLastTimestamp);
}
timeCursor = nextWeekCursor;
thisWeekCursor = nextWeekCursor;
}
emit TokenCheckpointed(block.timestamp, toDistribute);
}
/// @notice Update token checkpoint
/// @dev Calculate the total token to be distributed in a given week.
function checkpointToken() external nonReentrant {
_checkpointToken();
}
/// @notice Record StakeWeight total supply for each week
function _checkpointTotalSupply() internal {
StakeWeight stakeWeight = StakeWeight(config.getStakeWeight());
uint256 weekCursor_ = weekCursor;
uint256 roundedTimestamp = _timestampToFloorWeek(block.timestamp);
stakeWeight.checkpoint();
for (uint256 i = 0; i < MAX_REWARD_ITERATIONS; i++) {
if (weekCursor_ > roundedTimestamp) {
break;
} else {
uint256 epoch = _findTimestampEpoch(weekCursor_);
StakeWeight.Point memory point = stakeWeight.pointHistory(epoch);
int128 timeDelta = 0;
if (weekCursor_ > point.timestamp) {
timeDelta = SafeCast.toInt128(int256(weekCursor_ - point.timestamp));
}
int128 bias = point.bias - point.slope * timeDelta;
uint256 decaying = bias < 0 ? 0 : SafeCast.toUint256(bias);
// Add historical permanent snapshot aligned to this epoch
uint256 permanentAt = stakeWeight.permanentSupplyByEpoch(epoch);
totalSupplyAt[weekCursor_] = decaying + permanentAt;
}
weekCursor_ = weekCursor_ + 1 weeks;
}
weekCursor = weekCursor_;
}
/// @notice Calculate total supply at a specific timestamp (not cached)
/// @dev This ensures we always get the correct supply even after conversions
function _calculateTotalSupplyAt(uint256 timestamp) internal view returns (uint256) {
StakeWeight stakeWeight = StakeWeight(config.getStakeWeight());
uint256 epoch = _findTimestampEpoch(timestamp);
StakeWeight.Point memory point = stakeWeight.pointHistory(epoch);
int128 timeDelta = 0;
if (timestamp > point.timestamp) {
timeDelta = SafeCast.toInt128(int256(timestamp - point.timestamp));
}
int128 bias = point.bias - point.slope * timeDelta;
uint256 decaying = bias < 0 ? 0 : SafeCast.toUint256(bias);
// Add permanent supply at this epoch
uint256 permanentAt = stakeWeight.permanentSupplyByEpoch(epoch);
return decaying + permanentAt;
}
/// @notice Update StakeWeight total supply checkpoint
/// @dev This function can be called independently or at the first claim of
/// the new epoch week.
function checkpointTotalSupply() external nonReentrant {
_checkpointTotalSupply();
emit TotalSupplyCheckpointed(block.timestamp);
}
/// @notice Claim rewardToken
/// @dev Perform claim rewardToken
function _claim(address user, address recipient_, uint256 maxClaimTimestamp) internal returns (uint256) {
StakeWeight stakeWeight = StakeWeight(config.getStakeWeight());
uint256 userEpoch = 0;
uint256 toDistribute = 0;
uint256 maxUserEpoch = stakeWeight.userPointEpoch(user);
uint256 startWeekCursor_ = startWeekCursor;
// maxUserEpoch = 0, meaning no lock.
// Hence, no yield for user
if (maxUserEpoch == 0) {
return 0;
}
uint256 userWeekCursor = weekCursorOf[user];
if (userWeekCursor == 0) {
// if user has no userWeekCursor with GrassHouse yet
// then we need to perform binary search
userEpoch = _findTimestampUserEpoch(user, startWeekCursor_, maxUserEpoch);
} else {
// else, user must has epoch with GrassHouse already
userEpoch = userEpochOf[user];
}
if (userEpoch == 0) {
userEpoch = 1;
}
StakeWeight.Point memory userPoint = stakeWeight.userPointHistory(user, userEpoch);
if (userWeekCursor == 0) {
// If user locked exactly at week boundary, include that week
// Otherwise, round up to next week (no partial week rewards)
uint256 userTimestamp = userPoint.timestamp;
uint256 weekTimestamp = (userTimestamp / 1 weeks) * 1 weeks;
if (userTimestamp == weekTimestamp) {
// User locked exactly at week boundary, include this week
userWeekCursor = weekTimestamp;
} else {
// User locked mid-week, start from next week
userWeekCursor = weekTimestamp + 1 weeks;
}
}
// userWeekCursor is already at/beyond maxClaimTimestamp
// meaning nothing to be claimed for this user.
// This can be:
// 1) User just lock their WCT less than 1 week
// 2) User already claimed their rewards
if (userWeekCursor >= maxClaimTimestamp) {
return 0;
}
// Handle when user lock WCT before StakeWeight started
// by assign userWeekCursor to StakeWeight's startWeekCursor_
if (userWeekCursor < startWeekCursor_) {
userWeekCursor = startWeekCursor_;
}
StakeWeight.Point memory prevUserPoint = StakeWeight.Point({ bias: 0, slope: 0, timestamp: 0, blockNumber: 0 });
// Go through weeks
for (uint256 i = 0; i < MAX_REWARD_ITERATIONS; i++) {
// If userWeekCursor is iterated to be at/beyond maxClaimTimestamp
// This means we went through all weeks that user subject to claim rewards already
if (userWeekCursor >= maxClaimTimestamp) {
break;
}
// Calculate balance for current week BEFORE moving to new epoch
// This properly handles both permanent and decaying locks
uint256 balanceOf = this.balanceOfAt(user, userWeekCursor);
if (balanceOf > 0 && tokensPerWeek[userWeekCursor] > 0) {
// Calculate total supply on-the-fly to handle mid-week conversions correctly
uint256 totalSupply = _calculateTotalSupplyAt(userWeekCursor);
if (totalSupply > 0) {
toDistribute = toDistribute + (balanceOf * tokensPerWeek[userWeekCursor]) / totalSupply;
}
}
// Move to the new epoch if needed
if (userWeekCursor >= userPoint.timestamp && userEpoch <= maxUserEpoch) {
userEpoch = userEpoch + 1;
prevUserPoint = StakeWeight.Point({
bias: userPoint.bias,
slope: userPoint.slope,
timestamp: userPoint.timestamp,
blockNumber: userPoint.blockNumber
});
// When userEpoch goes beyond maxUserEpoch then there is no more Point,
// else take userEpoch as a new Point
if (userEpoch > maxUserEpoch) {
userPoint = StakeWeight.Point({ bias: 0, slope: 0, timestamp: 0, blockNumber: 0 });
} else {
userPoint = stakeWeight.userPointHistory(user, userEpoch);
}
}
// Check if we should stop
if (balanceOf == 0 && userEpoch > maxUserEpoch) {
break;
}
userWeekCursor = userWeekCursor + 1 weeks;
}
userEpoch = Math128.min(maxUserEpoch, userEpoch - 1);
userEpochOf[user] = userEpoch;
weekCursorOf[user] = userWeekCursor;
emit RewardsClaimed(user, recipient_, toDistribute, userEpoch, maxUserEpoch);
return toDistribute;
}
/// @notice Get claim recipient address
/// @param user The address to claim rewards for
function getRecipient(address user) public view returns (address recipient_) {
recipient_ = user;
address userRecipient = recipient[recipient_];
if (userRecipient != address(0)) {
recipient_ = userRecipient;
}
}
/// @notice Claim rewardToken for user and user's recipient
/// @param recipient_ The recipient address will be claimed to
function claimTo(address recipient_) external nonReentrant onlyLive returns (uint256) {
// Prevent claiming to arbitrary address if transfer restrictions are enabled
if (L2WCT(config.getL2wct()).transferRestrictionsDisabledAfter() >= block.timestamp) {
revert TransferRestrictionsEnabled();
}
return _claimWithCustomRecipient(msg.sender, recipient_);
}
/// @notice Claim rewardToken for user and user's recipient
/// @param user The address to claim rewards for
function claim(address user) external nonReentrant onlyLive returns (uint256) {
if (msg.sender != user) {
if (msg.sender != recipient[user]) revert UnauthorizedClaimer();
}
return _claimWithCustomRecipient(user, address(0));
}
function _claimWithCustomRecipient(address user, address recipient_) internal returns (uint256) {
if (block.timestamp >= weekCursor) _checkpointTotalSupply();
uint256 lastTokenTimestamp_ = lastTokenTimestamp;
_checkpointToken();
lastTokenTimestamp_ = block.timestamp;
lastTokenTimestamp_ = _timestampToFloorWeek(lastTokenTimestamp_);
if (recipient_ == address(0)) {
recipient_ = getRecipient(user);
}
uint256 total = _claim(user, recipient_, lastTokenTimestamp_);
if (total != 0) {
lastTokenBalance = lastTokenBalance - total;
IERC20(config.getL2wct()).safeTransfer(recipient_, total);
}
return total;
}
/// @notice Do Binary Search to find out epoch from timestamp
/// @param timestamp Timestamp to find epoch
function _findTimestampEpoch(uint256 timestamp) internal view returns (uint256) {
StakeWeight stakeWeight = StakeWeight(config.getStakeWeight());
uint256 min = 0;
uint256 max = stakeWeight.epoch();
// Loop for 128 times -> enough for 128-bit numbers
for (uint256 i = 0; i < MAX_BINARY_SEARCH_ITERATIONS; i++) {
if (min >= max) {
break;
}
uint256 mid = (min + max + 1) / 2;
StakeWeight.Point memory point = stakeWeight.pointHistory(mid);
if (point.timestamp <= timestamp) {
min = mid;
} else {
max = mid - 1;
}
}
return min;
}
/// @notice Perform binary search to find out user's epoch from the given timestamp
/// @param user The user address
/// @param timestamp The timestamp that you wish to find out epoch
/// @param maxUserEpoch Max epoch to find out the timestamp
function _findTimestampUserEpoch(
address user,
uint256 timestamp,
uint256 maxUserEpoch
)
internal
view
returns (uint256)
{
uint256 min = 0;
uint256 max = maxUserEpoch;
for (uint256 i = 0; i < MAX_BINARY_SEARCH_ITERATIONS; i++) {
if (min >= max) {
break;
}
uint256 mid = (min + max + 1) / 2;
StakeWeight.Point memory point = StakeWeight(config.getStakeWeight()).userPointHistory(user, mid);
if (point.timestamp <= timestamp) {
min = mid;
} else {
max = mid - 1;
}
}
return min;
}
/// @notice Emergency stop the contract and transfer remaining tokens to the emergency return address
function kill() external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) {
IERC20 rewardToken = IERC20(config.getL2wct());
isKilled = true;
rewardToken.safeTransfer(emergencyReturn, rewardToken.balanceOf(address(this)));
emit Killed();
}
/// @notice Round off random timestamp to week
/// @param timestamp The timestamp to be rounded off
function _timestampToFloorWeek(uint256 timestamp) internal pure returns (uint256) {
return (timestamp / 1 weeks) * 1 weeks;
}
/// @notice Inject rewardToken into the contract
/// @dev IMPORTANT: Tokens must be injected via this function to be claimable.
/// Tokens sent directly to the contract (not via injectReward) are not mapped
/// to tokensPerWeek and remain locked until the contract is killed via kill().
/// @param timestamp The timestamp of the rewardToken to be distributed
/// @param amount The amount of rewardToken to be distributed
function injectReward(uint256 timestamp, uint256 amount) external nonReentrant onlyRole(REWARD_MANAGER_ROLE) onlyLive {
_injectReward(timestamp, amount);
}
/// @notice Inject rewardToken for currect week into the contract
/// @param amount The amount of rewardToken to be distributed
function injectRewardForCurrentWeek(uint256 amount) external nonReentrant onlyRole(REWARD_MANAGER_ROLE) onlyLive {
_injectReward(block.timestamp, amount);
}
function _injectReward(uint256 timestamp, uint256 amount) internal {
uint256 weekTimestamp = _timestampToFloorWeek(timestamp);
if (weekTimestamp < startWeekCursor) {
revert InvalidTimestamp();
}
IERC20(config.getL2wct()).safeTransferFrom(msg.sender, address(this), amount);
lastTokenBalance += amount;
totalDistributed += amount;
tokensPerWeek[weekTimestamp] += amount;
emit RewardInjected(weekTimestamp, amount);
}
/// @notice Set recipient address
/// @param recipient_ Recipient address
function setRecipient(address recipient_) external {
// Prevent setting recipient if transfer restrictions are enabled
if (L2WCT(config.getL2wct()).transferRestrictionsDisabledAfter() >= block.timestamp) {
revert TransferRestrictionsEnabled();
}
address oldRecipient = recipient[msg.sender];
recipient[msg.sender] = recipient_;
emit RecipientUpdated(msg.sender, oldRecipient, recipient_);
}
}