| 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 |
| // SPDX-License-Identifier: MIT
pragma solidity >=0.8.4 <0.9.0;
import './Keep3rJobOwnership.sol';
import '../Keep3rAccountance.sol';
import '../Keep3rParameters.sol';
import '../../../interfaces/IPairManager.sol';
import '../../../interfaces/peripherals/IKeep3rJobs.sol';
import '../../libraries/FullMath.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/utils/math/Math.sol';
abstract contract Keep3rJobFundableLiquidity is IKeep3rJobFundableLiquidity, ReentrancyGuard, Keep3rJobOwnership, Keep3rParameters {
using EnumerableSet for EnumerableSet.AddressSet;
using SafeERC20 for IERC20;
/// @notice List of liquidities that are accepted in the system
EnumerableSet.AddressSet internal _approvedLiquidities;
/// @inheritdoc IKeep3rJobFundableLiquidity
mapping(address => mapping(address => uint256)) public override liquidityAmount;
/// @inheritdoc IKeep3rJobFundableLiquidity
mapping(address => uint256) public override rewardedAt;
/// @inheritdoc IKeep3rJobFundableLiquidity
mapping(address => uint256) public override workedAt;
/// @notice Tracks an address and returns its TickCache
mapping(address => TickCache) internal _tick;
// Views
/// @inheritdoc IKeep3rJobFundableLiquidity
function approvedLiquidities() external view override returns (address[] memory _list) {
_list = _approvedLiquidities.values();
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function jobPeriodCredits(address _job) public view virtual override returns (uint256 _periodCredits) {
for (uint256 i; i < _jobLiquidities[_job].length(); i++) {
address _liquidity = _jobLiquidities[_job].at(i);
if (_approvedLiquidities.contains(_liquidity)) {
TickCache memory _tickCache = observeLiquidity(_liquidity);
if (_tickCache.period != 0) {
int56 _tickDifference = _isKP3RToken0[_liquidity] ? _tickCache.difference : -_tickCache.difference;
_periodCredits += _getReward(
IKeep3rHelper(keep3rHelper).getKP3RsAtTick(liquidityAmount[_job][_liquidity], _tickDifference, rewardPeriodTime)
);
}
}
}
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function jobLiquidityCredits(address _job) public view override returns (uint256 _liquidityCredits) {
uint256 _periodCredits = jobPeriodCredits(_job);
// A job can have liquidityCredits without periodCredits (forced by Governance)
if (rewardedAt[_job] > _period(block.timestamp - rewardPeriodTime)) {
// Will calculate job credits only if it was rewarded later than last period
if ((block.timestamp - rewardedAt[_job]) >= rewardPeriodTime) {
// Will return a full period if job was rewarded more than a period ago
_liquidityCredits = _periodCredits;
} else {
// Will update minted job credits (not forced) to new twaps if credits are outdated
_liquidityCredits = _periodCredits > 0
? (_jobLiquidityCredits[_job] * _periodCredits) / _jobPeriodCredits[_job]
: _jobLiquidityCredits[_job];
}
} else {
// Will return a full period if job credits are expired
_liquidityCredits = _periodCredits;
}
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function totalJobCredits(address _job) external view override returns (uint256 _credits) {
uint256 _periodCredits = jobPeriodCredits(_job);
uint256 _cooldown;
if ((rewardedAt[_job] > _period(block.timestamp - rewardPeriodTime))) {
// Will calculate cooldown if it outdated
if ((block.timestamp - rewardedAt[_job]) >= rewardPeriodTime) {
// Will calculate cooldown from last reward reference in this period
_cooldown = block.timestamp - (rewardedAt[_job] + rewardPeriodTime);
} else {
// Will calculate cooldown from last reward timestamp
_cooldown = block.timestamp - rewardedAt[_job];
}
} else {
// Will calculate cooldown from period start if expired
_cooldown = block.timestamp - _period(block.timestamp);
}
_credits = jobLiquidityCredits(_job) + _phase(_cooldown, _periodCredits);
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function quoteLiquidity(address _liquidity, uint256 _amount) external view virtual override returns (uint256 _periodCredits) {
if (_approvedLiquidities.contains(_liquidity)) {
TickCache memory _tickCache = observeLiquidity(_liquidity);
if (_tickCache.period != 0) {
int56 _tickDifference = _isKP3RToken0[_liquidity] ? _tickCache.difference : -_tickCache.difference;
return _getReward(IKeep3rHelper(keep3rHelper).getKP3RsAtTick(_amount, _tickDifference, rewardPeriodTime));
}
}
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function observeLiquidity(address _liquidity) public view virtual override returns (TickCache memory _tickCache) {
if (_tick[_liquidity].period == _period(block.timestamp)) {
// Will return cached twaps if liquidity is updated
_tickCache = _tick[_liquidity];
} else {
bool success;
uint256 lastPeriod = _period(block.timestamp - rewardPeriodTime);
if (_tick[_liquidity].period == lastPeriod) {
// Will only ask for current period accumulator if liquidity is outdated
uint32[] memory _secondsAgo = new uint32[](1);
int56 previousTick = _tick[_liquidity].current;
_secondsAgo[0] = uint32(block.timestamp - _period(block.timestamp));
(_tickCache.current, , success) = IKeep3rHelper(keep3rHelper).observe(_liquidityPool[_liquidity], _secondsAgo);
_tickCache.difference = _tickCache.current - previousTick;
} else if (_tick[_liquidity].period < lastPeriod) {
// Will ask for 2 accumulators if liquidity is expired
uint32[] memory _secondsAgo = new uint32[](2);
_secondsAgo[0] = uint32(block.timestamp - _period(block.timestamp));
_secondsAgo[1] = uint32(block.timestamp - _period(block.timestamp) + rewardPeriodTime);
int56 _tickCumulative2;
(_tickCache.current, _tickCumulative2, success) = IKeep3rHelper(keep3rHelper).observe(_liquidityPool[_liquidity], _secondsAgo);
_tickCache.difference = _tickCache.current - _tickCumulative2;
}
if (success) {
_tickCache.period = _period(block.timestamp);
} else {
_tickCache.period = 0;
}
}
}
// Methods
/// @inheritdoc IKeep3rJobFundableLiquidity
function forceLiquidityCreditsToJob(address _job, uint256 _amount) external override onlyGovernance {
if (!_jobs.contains(_job)) revert JobUnavailable();
_settleJobAccountance(_job);
_jobLiquidityCredits[_job] += _amount;
emit LiquidityCreditsForced(_job, rewardedAt[_job], _jobLiquidityCredits[_job]);
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function approveLiquidity(address _liquidity) external virtual override onlyGovernance {
if (!_approvedLiquidities.add(_liquidity)) revert LiquidityPairApproved();
_liquidityPool[_liquidity] = IPairManager(_liquidity).pool();
_isKP3RToken0[_liquidity] = IKeep3rHelper(keep3rHelper).isKP3RToken0(_liquidityPool[_liquidity]);
_tick[_liquidity] = observeLiquidity(_liquidity);
emit LiquidityApproval(_liquidity);
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function revokeLiquidity(address _liquidity) external override onlyGovernance {
if (!_approvedLiquidities.remove(_liquidity)) revert LiquidityPairUnexistent();
emit LiquidityRevocation(_liquidity);
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function addLiquidityToJob(
address _job,
address _liquidity,
uint256 _amount
) external override nonReentrant {
if (!_approvedLiquidities.contains(_liquidity)) revert LiquidityPairUnapproved();
if (!_jobs.contains(_job)) revert JobUnavailable();
_jobLiquidities[_job].add(_liquidity);
_settleJobAccountance(_job);
if (_quoteLiquidity(liquidityAmount[_job][_liquidity] + _amount, _liquidity) < liquidityMinimum) revert JobLiquidityLessThanMin();
emit LiquidityCreditsReward(_job, rewardedAt[_job], _jobLiquidityCredits[_job], _jobPeriodCredits[_job]);
IERC20(_liquidity).safeTransferFrom(msg.sender, address(this), _amount);
liquidityAmount[_job][_liquidity] += _amount;
_jobPeriodCredits[_job] += _getReward(_quoteLiquidity(_amount, _liquidity));
emit LiquidityAddition(_job, _liquidity, msg.sender, _amount);
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function unbondLiquidityFromJob(
address _job,
address _liquidity,
uint256 _amount
) external override onlyJobOwner(_job) {
canWithdrawAfter[_job][_liquidity] = block.timestamp + unbondTime;
pendingUnbonds[_job][_liquidity] += _amount;
_unbondLiquidityFromJob(_job, _liquidity, _amount);
uint256 _remainingLiquidity = liquidityAmount[_job][_liquidity];
if (_remainingLiquidity > 0 && _quoteLiquidity(_remainingLiquidity, _liquidity) < liquidityMinimum) revert JobLiquidityLessThanMin();
emit Unbonding(_job, _liquidity, _amount);
}
/// @inheritdoc IKeep3rJobFundableLiquidity
function withdrawLiquidityFromJob(
address _job,
address _liquidity,
address _receiver
) external override onlyJobOwner(_job) {
if (_receiver == address(0)) revert ZeroAddress();
if (canWithdrawAfter[_job][_liquidity] == 0) revert UnbondsUnexistent();
if (canWithdrawAfter[_job][_liquidity] >= block.timestamp) revert UnbondsLocked();
if (disputes[_job]) revert Disputed();
uint256 _amount = pendingUnbonds[_job][_liquidity];
IERC20(_liquidity).safeTransfer(_receiver, _amount);
emit LiquidityWithdrawal(_job, _liquidity, _receiver, _amount);
pendingUnbonds[_job][_liquidity] = 0;
}
// Internal functions
/// @notice Updates or rewards job liquidity credits depending on time since last job reward
function _updateJobCreditsIfNeeded(address _job) internal returns (bool _rewarded) {
if (rewardedAt[_job] < _period(block.timestamp)) {
// Will exit function if job has been rewarded in current period
if (rewardedAt[_job] <= _period(block.timestamp - rewardPeriodTime)) {
// Will reset job to period syncronicity if a full period passed without rewards
_updateJobPeriod(_job);
_jobLiquidityCredits[_job] = _jobPeriodCredits[_job];
rewardedAt[_job] = _period(block.timestamp);
_rewarded = true;
} else if ((block.timestamp - rewardedAt[_job]) >= rewardPeriodTime) {
// Will reset job's syncronicity if last reward was more than epoch ago
_updateJobPeriod(_job);
_jobLiquidityCredits[_job] = _jobPeriodCredits[_job];
rewardedAt[_job] += rewardPeriodTime;
_rewarded = true;
} else if (workedAt[_job] < _period(block.timestamp)) {
// First keeper on period has to update job accountance to current twaps
uint256 previousPeriodCredits = _jobPeriodCredits[_job];
_updateJobPeriod(_job);
_jobLiquidityCredits[_job] = (_jobLiquidityCredits[_job] * _jobPeriodCredits[_job]) / previousPeriodCredits;
// Updating job accountance does not reward job
}
}
}
/// @notice Only called if _jobLiquidityCredits < payment
function _rewardJobCredits(address _job) internal {
/// @notice Only way to += jobLiquidityCredits is when keeper rewarding (cannot pay work)
/* WARNING: this allows to top up _jobLiquidityCredits to a max of 1.99 but have to spend at least 1 */
_jobLiquidityCredits[_job] += _phase(block.timestamp - rewardedAt[_job], _jobPeriodCredits[_job]);
rewardedAt[_job] = block.timestamp;
}
/// @notice Updates accountance for _jobPeriodCredits
function _updateJobPeriod(address _job) internal {
_jobPeriodCredits[_job] = _calculateJobPeriodCredits(_job);
}
/// @notice Quotes the outdated job liquidities and calculates _periodCredits
/// @dev This function is also responsible for keeping the KP3R/WETH quote updated
function _calculateJobPeriodCredits(address _job) internal virtual returns (uint256 _periodCredits) {
if (_tick[kp3rWethPool].period != _period(block.timestamp)) {
// Updates KP3R/WETH quote if needed
_tick[kp3rWethPool] = observeLiquidity(kp3rWethPool);
}
for (uint256 i; i < _jobLiquidities[_job].length(); i++) {
address _liquidity = _jobLiquidities[_job].at(i);
if (_approvedLiquidities.contains(_liquidity)) {
if (_tick[_liquidity].period != _period(block.timestamp)) {
// Updates liquidity cache only if needed
_tick[_liquidity] = observeLiquidity(_liquidity);
}
_periodCredits += _getReward(_quoteLiquidity(liquidityAmount[_job][_liquidity], _liquidity));
}
}
}
/// @notice Updates job accountance calculating the impact of the unbonded liquidity amount
function _unbondLiquidityFromJob(
address _job,
address _liquidity,
uint256 _amount
) internal nonReentrant {
if (!_jobLiquidities[_job].contains(_liquidity)) revert JobLiquidityUnexistent();
if (liquidityAmount[_job][_liquidity] < _amount) revert JobLiquidityInsufficient();
// Ensures current twaps in job liquidities
_updateJobPeriod(_job);
uint256 _periodCreditsToRemove = _getReward(_quoteLiquidity(_amount, _liquidity));
// A liquidity can be revoked causing a job to have 0 periodCredits
if (_jobPeriodCredits[_job] > 0) {
// Removes a % correspondant to a full rewardPeriodTime for the liquidity withdrawn vs all of the liquidities
_jobLiquidityCredits[_job] -= (_jobLiquidityCredits[_job] * _periodCreditsToRemove) / _jobPeriodCredits[_job];
_jobPeriodCredits[_job] -= _periodCreditsToRemove;
}
liquidityAmount[_job][_liquidity] -= _amount;
if (liquidityAmount[_job][_liquidity] == 0) {
_jobLiquidities[_job].remove(_liquidity);
}
}
/// @notice Returns a fraction of the multiplier or the whole multiplier if equal or more than a rewardPeriodTime has passed
function _phase(uint256 _timePassed, uint256 _multiplier) internal view returns (uint256 _result) {
if (_timePassed < rewardPeriodTime) {
_result = (_timePassed * _multiplier) / rewardPeriodTime;
} else _result = _multiplier;
}
/// @notice Returns the start of the period of the provided timestamp
function _period(uint256 _timestamp) internal view returns (uint256 _periodTimestamp) {
return _timestamp - (_timestamp % rewardPeriodTime);
}
/// @notice Calculates relation between rewardPeriod and inflationPeriod
function _getReward(uint256 _baseAmount) internal view returns (uint256 _credits) {
return FullMath.mulDiv(_baseAmount, rewardPeriodTime, inflationPeriod);
}
/// @notice Returns underlying KP3R amount for a given liquidity amount
function _quoteLiquidity(uint256 _amount, address _liquidity) internal view virtual returns (uint256 _quote) {
if (_tick[_liquidity].period != 0) {
int56 _tickDifference = _isKP3RToken0[_liquidity] ? _tick[_liquidity].difference : -_tick[_liquidity].difference;
_quote = IKeep3rHelper(keep3rHelper).getKP3RsAtTick(_amount, _tickDifference, rewardPeriodTime);
}
}
/// @notice Returns KP3R amount for a given ETH amount
function _quoteKp3rs(uint256 _eth) internal view virtual returns (uint256 _amountOut) {
int56 _tickDifference = _isKP3RToken0[kp3rWethPool] ? _tick[kp3rWethPool].difference : -_tick[kp3rWethPool].difference;
_amountOut = IKeep3rHelper(keep3rHelper).getQuoteAtTick(uint128(_eth), _tickDifference, rewardPeriodTime);
}
/// @notice Updates job credits to current quotes and rewards job's pending minted credits
/// @dev Ensures a maximum of 1 period of credits
function _settleJobAccountance(address _job) internal virtual {
_updateJobCreditsIfNeeded(_job);
_rewardJobCredits(_job);
_jobLiquidityCredits[_job] = Math.min(_jobLiquidityCredits[_job], _jobPeriodCredits[_job]);
}
}
|