-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathClisBNBLaunchPoolDistributor.sol
More file actions
235 lines (192 loc) · 9.08 KB
/
Copy pathClisBNBLaunchPoolDistributor.sol
File metadata and controls
235 lines (192 loc) · 9.08 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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../MerkleVerifier.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* @title ClisBNBLaunchPoolDistributor
* @author Lista
* @dev Distribute rewards to users based on their clisBNB balance which is calculated by off chain service
*/
contract ClisBNBLaunchPoolDistributor is Initializable, AccessControlUpgradeable {
using SafeERC20 for IERC20;
event UpdateEpoch(uint64 epochId, bytes32 merkleRoot, address token, uint256 startTime, uint256 endTime, uint256 amount);
event RevokeEpoch(uint64 epochId, address token, uint256 totalAmount, uint256 unclaimedAmount);
event CollectUnclaimed(uint64 epochId, address token, uint256 totalAmount, uint256 unclaimedAmount);
event Claimed(address account, uint64 epochId, address token, uint256 amount);
event TopUpEpoch(uint64 epochId, address token, uint256 oldTotalAmount, uint256 newTotalAmount, uint256 addedAmount);
struct Epoch {
// merkle root of an epoch
bytes32 merkleRoot;
// reward token of an epoch, address(0) means BNB
address token;
// start time of an epoch
uint256 startTime;
// start time of an epoch
uint256 endTime;
// total reward amount of an epoch
uint256 totalAmount;
// unclaimed reward amount of an epoch
uint256 unclaimedAmount;
}
// epochId => (merkleRoot, reward)
// epochId is the id of setting merkle root
// merkleRoot is the root of the merkle tree for the reward epoch
// epochReward is the total reward for the epoch
mapping(uint64 => Epoch) public epochs;
// epochId => userAddress => claimed
mapping(uint64 => mapping(address => bool)) public claimed;
// auto increment id for epoch
uint64 public nextEpochId;
// since merkleRoot/epochReward can be updated/revoked within the same week of setting (one week dispute period),
// we need to keep track of the unsettled reward
// token => unclaimedAmount
mapping(address => uint256) public totalUnclaimedAmount;
// OPERATOR role
bytes32 public constant OPERATOR = keccak256("OPERATOR");
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/**
* @param _admin Address of the admin
*/
function initialize(address _admin) external initializer {
require(_admin != address(0), "Invalid admin address");
_setupRole(DEFAULT_ADMIN_ROLE, _admin);
}
/**
* @dev Claim Lista rewards. Can be called by anyone as long as proof is valid.
* @param _epochId Id of epoch
* @param _account Address of the recipient
* @param _amount Reward amount of Lista to user
* @param _proof Merkle proof of the claim
*/
function claim(
uint64 _epochId,
address _account,
uint256 _amount,
bytes32[] memory _proof
) external {
require(_amount > 0, "Invalid amount");
require(!claimed[_epochId][_account], "User already claimed");
Epoch storage epoch = epochs[_epochId];
require(epoch.merkleRoot != bytes32(0), "Invalid epochId");
require(epoch.startTime <= block.timestamp && epoch.endTime >= block.timestamp, "Inactive epoch");
bytes32 leaf = keccak256(abi.encode(block.chainid, _epochId, _account, _amount));
MerkleVerifier._verifyProof(leaf, epoch.merkleRoot, _proof);
claimed[_epochId][_account] = true;
totalUnclaimedAmount[epoch.token] -= _amount;
epoch.unclaimedAmount -= _amount;
_transferTo(_account, epoch.token, _amount);
emit Claimed(_account, _epochId, epoch.token, _amount);
}
/**
* @dev Set merkle root for rewards epoch.
* @param _epochId Epoch Id of the reward epoch
* @param _merkleRoot Merkle root of the reward epoch
* @param _token Reward token of the reward epoch, address(0) means BNB
* @param _startTime Start time of the reward epoch
* @param _endTime End time of the reward epoch
* @param _totalAmount Total amount of the reward epoch
*/
function setEpochMerkleRoot(uint64 _epochId, bytes32 _merkleRoot, address _token, uint256 _startTime, uint256 _endTime, uint256 _totalAmount)
external onlyRole(OPERATOR)
{
require(_epochId == nextEpochId, "Invalid epochId");
require(_merkleRoot != bytes32(0), "Invalid merkle root");
require(_startTime > block.timestamp, "Invalid start time");
require(_endTime > _startTime, "Invalid end time");
require(_totalAmount > 0, "Invalid total amount");
uint64 currentEpochId = nextEpochId++;
Epoch storage epoch = epochs[currentEpochId];
epoch.merkleRoot = _merkleRoot;
epoch.token = _token;
epoch.startTime = _startTime;
epoch.endTime = _endTime;
epoch.totalAmount = _totalAmount;
epoch.unclaimedAmount = _totalAmount;
totalUnclaimedAmount[_token] += _totalAmount;
emit UpdateEpoch(currentEpochId, epoch.merkleRoot, epoch.token, epoch.startTime, epoch.endTime, epoch.totalAmount);
}
/**
* @dev Raise an active epoch's total to the correct amount (= sum of merkle leaves), fixing
* an under-set totalAmount that blocks tail users from claiming. Fund the delta first.
* @param _epochId Id of epoch
* @param _newTotalAmount New total amount of the epoch, must exceed the current totalAmount
*/
function topUpEpoch(uint64 _epochId, uint256 _newTotalAmount) external onlyRole(DEFAULT_ADMIN_ROLE) {
Epoch storage epoch = epochs[_epochId];
require(epoch.merkleRoot != bytes32(0), "Invalid epochId");
require(block.timestamp >= epoch.startTime, "Epoch not started");
require(block.timestamp <= epoch.endTime, "Epoch ended");
require(_newTotalAmount > epoch.totalAmount, "Not an increase");
uint256 oldTotalAmount = epoch.totalAmount;
uint256 added = _newTotalAmount - oldTotalAmount;
epoch.totalAmount = _newTotalAmount;
epoch.unclaimedAmount += added;
totalUnclaimedAmount[epoch.token] += added;
require(_balanceOf(epoch.token) >= totalUnclaimedAmount[epoch.token], "Insufficient funds");
emit TopUpEpoch(_epochId, epoch.token, oldTotalAmount, _newTotalAmount, added);
}
/**
* @dev Revoke the reward of the given epoch;
* @param _epochId Id of epoch
*/
function revokeEpoch(uint64 _epochId) external onlyRole(OPERATOR) {
Epoch storage epoch = epochs[_epochId];
require(epoch.startTime > block.timestamp, "Epoch already started");
require(epoch.totalAmount > 0, "Invalid epochId");
address token = epoch.token;
uint256 epochTotalAmount = epoch.totalAmount;
uint256 epochUnclaimedAmount = epoch.unclaimedAmount;
totalUnclaimedAmount[token] -= epochUnclaimedAmount;
delete epochs[_epochId];
emit RevokeEpoch(_epochId, token, epochTotalAmount, epochUnclaimedAmount);
}
/**
* @dev Collect unclaimed rewards amount of the given ended epoch
* @param _epochId Id of epoch
*/
function collectUnclaimed(uint64 _epochId) external onlyRole(DEFAULT_ADMIN_ROLE) {
Epoch storage epoch = epochs[_epochId];
require(epoch.totalAmount > 0, "Invalid epochId");
require(epoch.unclaimedAmount > 0, "No unclaimed amount");
require(epoch.endTime < block.timestamp, "Epoch not ended");
address token = epoch.token;
uint256 epochUnclaimedAmount = epoch.unclaimedAmount;
totalUnclaimedAmount[token] -= epochUnclaimedAmount;
epoch.unclaimedAmount = 0;
_transferTo(msg.sender, token, epochUnclaimedAmount);
emit CollectUnclaimed(_epochId, token, epoch.totalAmount, epochUnclaimedAmount);
}
/**
* @dev Transfer the given amount to the admin
* @param _amount Amount to transfer
*/
function adminTransfer(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(_amount > 0, "Invalid amount");
_transferTo(msg.sender, _token, _amount);
}
function _transferTo(address _to, address _token, uint256 _amount) private {
if (_token == address(0)) {
(bool success,) = payable(_to).call{value: _amount}("");
require(success, "Transfer BNB failed");
} else {
IERC20(_token).safeTransfer(_to, _amount);
}
}
function _balanceOf(address _token) private view returns (uint256) {
return _token == address(0) ? address(this).balance : IERC20(_token).balanceOf(address(this));
}
function getEpochs(uint64[] memory _epochIds) external view returns (Epoch[] memory) {
Epoch[] memory _epochs = new Epoch[](_epochIds.length);
for (uint256 i = 0; i < _epochIds.length; i++) {
_epochs[i] = epochs[_epochIds[i]];
}
return _epochs;
}
receive() external payable {}
}