forked from InverterNetwork/audit-hats
-
Notifications
You must be signed in to change notification settings - Fork 3
/
LM_PC_KPIRewarder_v1.sol
411 lines (341 loc) · 14.5 KB
/
LM_PC_KPIRewarder_v1.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
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
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.23;
// Internal Interfaces
import {IOrchestrator_v1} from
"src/orchestrator/interfaces/IOrchestrator_v1.sol";
import {ILM_PC_KPIRewarder_v1} from "@lm/interfaces/ILM_PC_KPIRewarder_v1.sol";
import {
ILM_PC_Staking_v1,
LM_PC_Staking_v1,
SafeERC20,
IERC20,
ERC20PaymentClientBase_v1
} from "./LM_PC_Staking_v1.sol";
import {
IOptimisticOracleIntegrator,
OptimisticOracleIntegrator,
OptimisticOracleV3CallbackRecipientInterface
} from
"src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/OptimisticOracleIntegrator.sol";
// Internal Dependencies
import {Module_v1} from "src/modules/base/Module_v1.sol";
/**
* @title KPI Rewarder Module
*
* @notice Provides a mechanism for distributing rewards to stakers based
* on Key Performance Indicators (KPIs).
*
* @dev Extends {LM_PC_Staking_v1} and integrates with {OptimisticOracleIntegrator}
* to enable KPI-based reward distribution within the staking manager.
*
* @custom:security-contact [email protected]
* In case of any concerns or findings, please refer to our Security Policy
* at security.inverter.network or email us directly!
*
* @author Inverter Network
*/
contract LM_PC_KPIRewarder_v1 is
ILM_PC_KPIRewarder_v1,
LM_PC_Staking_v1,
OptimisticOracleIntegrator
{
using SafeERC20 for IERC20;
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(LM_PC_Staking_v1, Module_v1)
returns (bool)
{
return interfaceId == type(ILM_PC_KPIRewarder_v1).interfaceId
|| super.supportsInterface(interfaceId);
}
//--------------------------------------------------------------------------
// General Information about the working of this contract
// This module enable KPI based reward distribution into the staking manager by using UMAs Optimistic Oracle.
// It works in the following way:
// - The admin can create KPIs, which are a set of tranches with rewards assigned. These can be continuous or not (see below)
// - An external actor with the ASSERTER role can trigger the posting of an assertion to the UMA Oracle, specifying the value to be asserted and the KPI to use for the reward distrbution in case it resolves
// - To ensure fairness, all new staking requests are queued until the next KPI assertion is resolved. They will be added before posting the next assertion.
// - Once the assertion resolves, the UMA oracle triggers the assertionResolvedCallback() function. This will calculate the final reward value and distribute it to the stakers.
//--------------------------------------------------------------------------
// KPI and Configuration Storage
uint public KPICounter;
mapping(uint => KPI) public registryOfKPIs;
mapping(bytes32 => RewardRoundConfiguration) public assertionConfig;
// Deposit Queue
bool public assertionPending;
uint minimumStake; // The workflow owner can set a minimum stake amount to mitigate griefing attacks where sybils spam the queue with multiple small stakes.
address[] public stakingQueue;
mapping(address => uint) public stakingQueueAmounts;
uint public totalQueuedFunds;
uint public constant MAX_QUEUE_LENGTH = 50;
// Storage gap for future upgrades
uint[50] private __gap;
/*
Tranche Example:
trancheValues = [10000, 20000, 30000]
trancheRewards = [100, 200, 100]
continuous = false
-> if KPI is 12345, reward is 100 for the tranche [0-10000]
-> if KPI is 32198, reward is 400 for the tranches [0-10000, 10000-20000 and 20000-30000]
if continuous = true
-> if KPI is 15000, reward is 200 for the tranches [100% 0-10000, 50% * 10000-15000]
-> if KPI is 25000, reward is 350 for the tranches [100% 0-10000, 100% 10000-20000, 50% 20000-30000]
*/
/// @inheritdoc Module_v1
function init(
IOrchestrator_v1 orchestrator_,
Metadata memory metadata,
bytes memory configData
)
external
virtual
override(LM_PC_Staking_v1, OptimisticOracleIntegrator)
initializer
{
__Module_init(orchestrator_, metadata);
(
address stakingTokenAddr,
address currencyAddr,
uint defaultBond,
address ooAddr,
uint64 liveness
) = abi.decode(configData, (address, address, uint, address, uint64));
__LM_PC_Staking_v1_init(stakingTokenAddr);
__OptimisticOracleIntegrator_init(
currencyAddr, defaultBond, ooAddr, liveness
);
}
//--------------------------------------------------------------------------
// View functions
/// @inheritdoc ILM_PC_KPIRewarder_v1
function getKPI(uint KPInum) public view returns (KPI memory) {
return registryOfKPIs[KPInum];
}
/// @inheritdoc ILM_PC_KPIRewarder_v1
function getAssertionConfig(bytes32 assertionId)
public
view
returns (RewardRoundConfiguration memory)
{
return assertionConfig[assertionId];
}
/// @inheritdoc ILM_PC_KPIRewarder_v1
function getStakingQueue() public view returns (address[] memory) {
return stakingQueue;
}
//--------------------------------------------------------------------------
// Assertion Manager Functions
/// @inheritdoc ILM_PC_KPIRewarder_v1
/// @dev about the asserter address: any address can be set as asserter, it will be expected to pay for the bond on posting.
/// The bond tokens can also be deposited in the Module and used to pay for itself, but ONLY if the bond token is different from the one being used for staking.
/// If the asserter is set to 0, whomever calls postAssertion will be paying the bond.
function postAssertion(
bytes32 dataId,
uint assertedValue,
address asserter,
uint targetKPI
) public onlyModuleRole(ASSERTER_ROLE) returns (bytes32 assertionId) {
if (assertionPending) {
revert Module__LM_PC_KPIRewarder_v1__UnresolvedAssertionExists();
}
//--------------------------------------------------------------------------
// Input Validation
// If the asserter is the Module itself, we need to ensure the token paid for bond is different than the one used for staking, since it could mess with the balances
if (
asserter == address(this)
&& address(defaultCurrency) == stakingToken
) {
revert
Module__LM_PC_KPIRewarder_v1__ModuleCannotUseStakingTokenAsBond();
}
// Make sure that we are targeting an existing KPI
if (KPICounter == 0 || targetKPI >= KPICounter) {
revert Module__LM_PC_KPIRewarder_v1__InvalidKPINumber();
}
//--------------------------------------------------------------------------
// Staking Queue Management
for (uint i = 0; i < stakingQueue.length; i++) {
address user = stakingQueue[i];
_stake(user, stakingQueueAmounts[user]);
totalQueuedFunds -= stakingQueueAmounts[user];
stakingQueueAmounts[user] = 0;
}
delete stakingQueue; // reset the queue
//--------------------------------------------------------------------------
// Assertion Posting
assertionId = assertDataFor(dataId, bytes32(assertedValue), asserter);
assertionConfig[assertionId] = RewardRoundConfiguration(
block.timestamp, assertedValue, targetKPI, false
);
emit RewardRoundConfigured(
assertionId, block.timestamp, assertedValue, targetKPI
);
assertionPending = true;
// (return assertionId)
}
//--------------------------------------------------------------------------
// Admin Configuration Functions:
// Top up funds to pay the optimistic oracle fee
/// @inheritdoc ILM_PC_KPIRewarder_v1
function depositFeeFunds(uint amount)
external
onlyOrchestratorAdmin
nonReentrant
validAmount(amount)
{
defaultCurrency.safeTransferFrom(_msgSender(), address(this), amount);
emit FeeFundsDeposited(address(defaultCurrency), amount);
}
/// @inheritdoc ILM_PC_KPIRewarder_v1
function createKPI(
bool _continuous,
uint[] calldata _trancheValues,
uint[] calldata _trancheRewards
) external onlyOrchestratorAdmin returns (uint) {
uint _numOfTranches = _trancheValues.length;
if (_numOfTranches < 1 || _numOfTranches > 20) {
revert Module__LM_PC_KPIRewarder_v1__InvalidTrancheNumber();
}
if (_numOfTranches != _trancheRewards.length) {
revert Module__LM_PC_KPIRewarder_v1__InvalidKPIValueLengths();
}
uint _totalKPIRewards = _trancheRewards[0];
if (_numOfTranches > 1) {
for (uint i = 1; i < _numOfTranches; i++) {
if (_trancheValues[i - 1] >= _trancheValues[i]) {
revert Module__LM_PC_KPIRewarder_v1__InvalidKPITrancheValues(
);
}
_totalKPIRewards += _trancheRewards[i];
}
}
uint KpiNum = KPICounter;
registryOfKPIs[KpiNum] = KPI(
_numOfTranches,
_totalKPIRewards,
_continuous,
_trancheValues,
_trancheRewards
);
KPICounter++;
emit KPICreated(
KpiNum,
_numOfTranches,
_totalKPIRewards,
_continuous,
_trancheValues,
_trancheRewards
);
return (KpiNum);
}
function setMinimumStake(uint _minimumStake)
external
onlyOrchestratorAdmin
{
minimumStake = _minimumStake;
}
//--------------------------------------------------------------------------
// New user facing functions (stake() is a LM_PC_Staking_v1 override) :
/// @inheritdoc ILM_PC_Staking_v1
function stake(uint amount)
external
override
nonReentrant
validAmount(amount)
{
if (stakingQueue.length >= MAX_QUEUE_LENGTH) {
revert Module__LM_PC_KPIRewarder_v1__StakingQueueIsFull();
}
if (amount < minimumStake) {
revert Module__LM_PC_KPIRewarder_v1__InvalidStakeAmount();
}
address sender = _msgSender();
if (stakingQueueAmounts[sender] == 0) {
// new stake for queue
stakingQueue.push(sender);
}
stakingQueueAmounts[sender] += amount;
totalQueuedFunds += amount;
// transfer funds to LM_PC_Staking_v1
IERC20(stakingToken).safeTransferFrom(sender, address(this), amount);
emit StakeEnqueued(sender, amount);
}
/// @inheritdoc ILM_PC_KPIRewarder_v1
function dequeueStake() public nonReentrant {
address user = _msgSender();
// keep it idempotent
if (stakingQueueAmounts[user] != 0) {
uint amount = stakingQueueAmounts[user];
stakingQueueAmounts[user] = 0;
totalQueuedFunds -= amount;
for (uint i; i < stakingQueue.length; i++) {
if (stakingQueue[i] == user) {
stakingQueue[i] = stakingQueue[stakingQueue.length - 1];
stakingQueue.pop();
break;
}
}
emit StakeDequeued(user, amount);
// return funds to user
IERC20(stakingToken).safeTransfer(user, amount);
}
}
//--------------------------------------------------------------------------
// Optimistic Oracle Overrides:
/// @inheritdoc OptimisticOracleV3CallbackRecipientInterface
function assertionResolvedCallback(
bytes32 assertionId,
bool assertedTruthfully
) public override {
// First, we perform checks and state management on the parent function.
super.assertionResolvedCallback(assertionId, assertedTruthfully);
// If the assertion was true, we calculate the rewards and distribute them.
if (assertedTruthfully) {
// SECURITY NOTE: this will add the value, but provides no guarantee that the fundingmanager actually holds those funds.
// Calculate rewardamount from assertionId value
KPI memory resolvedKPI =
registryOfKPIs[assertionConfig[assertionId].KpiToUse];
uint rewardAmount;
for (uint i; i < resolvedKPI.numOfTranches; i++) {
if (
resolvedKPI.trancheValues[i]
<= assertionConfig[assertionId].assertedValue
) {
// the asserted value is above tranche end
rewardAmount += resolvedKPI.trancheRewards[i];
} else {
// tranche was not completed
if (resolvedKPI.continuous) {
// continuous distribution
uint trancheRewardValue = resolvedKPI.trancheRewards[i];
uint trancheStart =
i == 0 ? 0 : resolvedKPI.trancheValues[i - 1];
uint achievedReward = assertionConfig[assertionId]
.assertedValue - trancheStart;
uint trancheEnd =
resolvedKPI.trancheValues[i] - trancheStart;
rewardAmount +=
achievedReward * (trancheRewardValue / trancheEnd); // since the trancheRewardValue will be a very big number.
}
// else -> no reward
// exit the loop
break;
}
}
_setRewards(rewardAmount, 1);
assertionConfig[assertionId].distributed = true;
} else {
// To keep in line with the upstream contract. If the assertion was false, we delete the corresponding assertionConfig from storage.
delete assertionConfig[assertionId];
}
// Independently of the fact that the assertion resolved true or not, new assertions can now be posted.
assertionPending = false;
}
/// @inheritdoc OptimisticOracleV3CallbackRecipientInterface
/// @dev This OptimisticOracleV3 callback function needs to be defined so the OOv3 doesn't revert when it tries to call it.
function assertionDisputedCallback(bytes32 assertionId) public override {
// Do nothing
}
}