• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NexusMutual / smart-contracts / #778

23 Jul 2024 01:54PM UTC coverage: 87.593% (+4.3%) from 83.289%
#778

push

MilGard91
Run compilation in the storage layout extraction script if invoked from cli (thx @fvictorio)

1023 of 1274 branches covered (80.3%)

Branch coverage included in aggregate %.

2987 of 3304 relevant lines covered (90.41%)

170.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.94
/contracts/modules/staking/StakingPool.sol
1
// SPDX-License-Identifier: GPL-3.0-only
2

3
pragma solidity ^0.8.18;
4

5
import "../../abstract/Multicall.sol";
6
import "../../interfaces/IStakingPool.sol";
7
import "../../interfaces/IStakingNFT.sol";
8
import "../../interfaces/ITokenController.sol";
9
import "../../interfaces/INXMMaster.sol";
10
import "../../interfaces/INXMToken.sol";
11
import "../../interfaces/IStakingProducts.sol";
12
import "../../libraries/Math.sol";
13
import "../../libraries/UncheckedMath.sol";
14
import "../../libraries/SafeUintCast.sol";
15
import "./StakingTypesLib.sol";
16

17
// total stake = active stake + expired stake
18
// total capacity = active stake * global capacity factor
19
// total product capacity = total capacity * capacity reduction factor * product weight
20
// total product capacity = allocated product capacity + available product capacity
21
// on cover buys we allocate the available product capacity
22
// on cover expiration we deallocate the capacity and it becomes available again
23

24
contract StakingPool is IStakingPool, Multicall {
25
  using StakingTypesLib for TrancheAllocationGroup;
26
  using StakingTypesLib for TrancheGroupBucket;
27
  using SafeUintCast for uint;
28
  using UncheckedMath for uint;
29

30
  /* storage */
31

32
  // slot 1
33
  // supply of pool stake shares used by tranches
34
  uint128 internal stakeSharesSupply;
35

36
  // supply of pool rewards shares used by tranches
37
  uint128 internal rewardsSharesSupply;
38

39
  // slot 2
40
  // accumulated rewarded nxm per reward share
41
  uint96 internal accNxmPerRewardsShare;
42

43
  // currently active staked nxm amount
44
  uint96 internal activeStake;
45

46
  uint32 internal firstActiveTrancheId;
47
  uint32 internal firstActiveBucketId;
48

49
  // slot 3
50
  // timestamp when accNxmPerRewardsShare was last updated
51
  uint32 internal lastAccNxmUpdate;
52
  // current nxm reward per second for the entire pool
53
  // applies to active stake only and does not need update on deposits
54
  uint96 internal rewardPerSecond;
55

56
  uint40 internal poolId;
57
  uint24 internal lastAllocationId;
58

59
  bool public override isPrivatePool;
60
  bool public override isHalted;
61

62
  uint8 internal poolFee;
63
  uint8 internal maxPoolFee;
64

65
  // 32 bytes left in slot 3
66

67
  // tranche id => tranche data
68
  mapping(uint => Tranche) internal tranches;
69

70
  // tranche id => expired tranche data
71
  mapping(uint => ExpiredTranche) internal expiredTranches;
72

73
  // reward bucket id => RewardBucket
74
  mapping(uint => uint) public rewardPerSecondCut;
75

76
  // product id => tranche group id => active allocations for a tranche group
77
  mapping(uint => mapping(uint => TrancheAllocationGroup)) public trancheAllocationGroups;
78

79
  // product id => bucket id => bucket tranche group id => tranche group's expiring cover amounts
80
  mapping(uint => mapping(uint => mapping(uint => TrancheGroupBucket))) public expiringCoverBuckets;
81

82
  // cover id => per tranche cover amounts (8 32-bit values, one per tranche, packed in a slot)
83
  // starts with the first active tranche at the time of cover buy
84
  mapping(uint => uint) public coverTrancheAllocations;
85

86
  // token id => tranche id => deposit data
87
  mapping(uint => mapping(uint => Deposit)) public deposits;
88

89
  /* immutables */
90

91
  IStakingNFT public immutable stakingNFT;
92
  INXMToken public immutable nxm;
93
  ITokenController public  immutable tokenController;
94
  address public immutable coverContract;
95
  INXMMaster public immutable masterContract;
96
  IStakingProducts public immutable stakingProducts;
97

98
  /* constants */
99

100
  // 7 * 13 = 91
101
  uint public constant BUCKET_DURATION = 28 days;
102
  uint public constant TRANCHE_DURATION = 91 days;
103
  uint public constant MAX_ACTIVE_TRANCHES = 8; // 7 whole quarters + 1 partial quarter
104

105
  uint public constant COVER_TRANCHE_GROUP_SIZE = 5;
106
  uint public constant BUCKET_TRANCHE_GROUP_SIZE = 8;
107

108
  uint public constant REWARD_BONUS_PER_TRANCHE_RATIO = 10_00; // 10.00%
109
  uint public constant REWARD_BONUS_PER_TRANCHE_DENOMINATOR = 100_00;
110
  uint public constant WEIGHT_DENOMINATOR = 100;
111
  uint public constant REWARDS_DENOMINATOR = 100_00;
112
  uint public constant POOL_FEE_DENOMINATOR = 100;
113

114
  // denominators for cover contract parameters
115
  uint public constant GLOBAL_CAPACITY_DENOMINATOR = 100_00;
116
  uint public constant CAPACITY_REDUCTION_DENOMINATOR = 100_00;
117

118
  // +2% for every 1%, ie +200% for 100%
119

120
  // 1 nxm = 1e18
121
  uint internal constant ONE_NXM = 1 ether;
122

123
  // internally we store capacity using 2 decimals
124
  // 1 nxm of capacity is stored as 100
125
  uint public constant ALLOCATION_UNITS_PER_NXM = 100;
126

127
  // given capacities have 2 decimals
128
  // smallest unit we can allocate is 1e18 / 100 = 1e16 = 0.01 NXM
129
  uint public constant NXM_PER_ALLOCATION_UNIT = ONE_NXM / ALLOCATION_UNITS_PER_NXM;
130

131
  modifier onlyCoverContract {
132
    if (msg.sender != coverContract) {
579✔
133
      revert OnlyCoverContract();
2✔
134
    }
135
    _;
577✔
136
  }
137

138
  modifier onlyManager {
139
    if (msg.sender != manager()) {
14✔
140
      revert OnlyManager();
2✔
141
    }
142
    _;
12✔
143
  }
144

145
  modifier whenNotPaused {
146
    if (masterContract.isPause()) {
272✔
147
      revert SystemPaused();
3✔
148
    }
149
    _;
269✔
150
  }
151

152
  modifier whenNotHalted {
153
    if (isHalted) {
246✔
154
      revert PoolHalted();
3✔
155
    }
156
    _;
243✔
157
  }
158

159
  constructor (
160
    address _stakingNFT,
161
    address _token,
162
    address _coverContract,
163
    address _tokenController,
164
    address _master,
165
    address _stakingProducts
166
  ) {
167
    stakingNFT = IStakingNFT(_stakingNFT);
12✔
168
    nxm = INXMToken(_token);
12✔
169
    coverContract = _coverContract;
12✔
170
    tokenController = ITokenController(_tokenController);
12✔
171
    masterContract = INXMMaster(_master);
12✔
172
    stakingProducts = IStakingProducts(_stakingProducts);
12✔
173
  }
174

175
  function initialize(
176
    bool _isPrivatePool,
177
    uint _initialPoolFee,
178
    uint _maxPoolFee,
179
    uint _poolId,
180
    string  calldata ipfsDescriptionHash
181
  ) external {
182

183
    if (msg.sender != address(stakingProducts)) {
31✔
184
      revert OnlyStakingProductsContract();
1✔
185
    }
186

187
    if (_initialPoolFee > _maxPoolFee) {
30✔
188
      revert PoolFeeExceedsMax();
1✔
189
    }
190

191
    if (_maxPoolFee >= 100) {
29✔
192
      revert MaxPoolFeeAbove100();
1✔
193
    }
194

195
    isPrivatePool = _isPrivatePool;
28✔
196
    poolFee = uint8(_initialPoolFee);
28✔
197
    maxPoolFee = uint8(_maxPoolFee);
28✔
198
    poolId = _poolId.toUint40();
28✔
199

200
    emit PoolDescriptionSet(ipfsDescriptionHash);
28✔
201
  }
202

203
  // updateUntilCurrentTimestamp forces rewards update until current timestamp not just until
204
  // bucket/tranche expiry timestamps. Must be true when changing shares or reward per second.
205
  function processExpirations(bool updateUntilCurrentTimestamp) public {
206

207
    uint _firstActiveBucketId = firstActiveBucketId;
738✔
208
    uint _firstActiveTrancheId = firstActiveTrancheId;
738✔
209

210
    uint currentBucketId = block.timestamp / BUCKET_DURATION;
738✔
211
    uint currentTrancheId = block.timestamp / TRANCHE_DURATION;
738✔
212

213
    // if the pool is new
214
    if (_firstActiveBucketId == 0) {
738✔
215
      _firstActiveBucketId = currentBucketId;
5✔
216
      _firstActiveTrancheId = currentTrancheId;
5✔
217
    }
218

219
    // if a force update was not requested
220
    if (!updateUntilCurrentTimestamp) {
738✔
221

222
      bool canExpireBuckets = _firstActiveBucketId < currentBucketId;
66✔
223
      bool canExpireTranches = _firstActiveTrancheId < currentTrancheId;
66✔
224

225
      // and if there's nothing to expire
226
      if (!canExpireBuckets && !canExpireTranches) {
66✔
227
        // we can exit
228
        return;
27✔
229
      }
230
    }
231

232
    // SLOAD
233
    uint _activeStake = activeStake;
711✔
234
    uint _rewardPerSecond = rewardPerSecond;
711✔
235
    uint _stakeSharesSupply = stakeSharesSupply;
711✔
236
    uint _rewardsSharesSupply = rewardsSharesSupply;
711✔
237
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
711✔
238
    uint _lastAccNxmUpdate = lastAccNxmUpdate;
711✔
239

240
    // exit early if we already updated in the current block
241
    if (_lastAccNxmUpdate == block.timestamp) {
711✔
242
      return;
12✔
243
    }
244

245
    while (_firstActiveBucketId < currentBucketId || _firstActiveTrancheId < currentTrancheId) {
699✔
246

247
      // what expires first, the bucket or the tranche?
248
      bool bucketExpiresFirst;
600✔
249
      {
600✔
250
        uint nextBucketStart = (_firstActiveBucketId + 1) * BUCKET_DURATION;
600✔
251
        uint nextTrancheStart = (_firstActiveTrancheId + 1) * TRANCHE_DURATION;
600✔
252
        bucketExpiresFirst = nextBucketStart <= nextTrancheStart;
600✔
253
      }
254

255
      if (bucketExpiresFirst) {
600✔
256

257
        // expire a bucket
258
        // each bucket contains a reward reduction - we subtract it when the bucket *starts*!
259

260
        ++_firstActiveBucketId;
433✔
261
        uint bucketStartTime = _firstActiveBucketId * BUCKET_DURATION;
433✔
262
        uint elapsed = bucketStartTime - _lastAccNxmUpdate;
433✔
263

264
        uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
433✔
265
          ? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
266
          : 0;
267

268
        _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
433✔
269

270
        _rewardPerSecond -= rewardPerSecondCut[_firstActiveBucketId];
433✔
271
        _lastAccNxmUpdate = bucketStartTime;
433✔
272

273
        emit BucketExpired(_firstActiveBucketId - 1);
433✔
274
        continue;
433✔
275
      }
276

277
      // expire a tranche
278
      // each tranche contains shares - we expire them when the tranche *ends*
279
      // TODO: check if we have to expire the tranche
280
      {
167✔
281
        uint trancheEndTime = (_firstActiveTrancheId + 1) * TRANCHE_DURATION;
167✔
282
        uint elapsed = trancheEndTime - _lastAccNxmUpdate;
167✔
283
        uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
167✔
284
          ? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
285
          : 0;
286
        _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
167✔
287
        _lastAccNxmUpdate = trancheEndTime;
167✔
288

289
        // SSTORE
290
        expiredTranches[_firstActiveTrancheId] = ExpiredTranche(
167✔
291
          _accNxmPerRewardsShare.toUint96(), // accNxmPerRewardShareAtExpiry
292
          _activeStake.toUint96(), // stakeAmountAtExpiry
293
          _stakeSharesSupply.toUint128() // stakeSharesSupplyAtExpiry
294
        );
295

296
        // SLOAD and then SSTORE zero to get the gas refund
297
        Tranche memory expiringTranche = tranches[_firstActiveTrancheId];
167✔
298
        delete tranches[_firstActiveTrancheId];
167✔
299

300
        // the tranche is expired now so we decrease the stake and the shares supply
301
        uint expiredStake = _stakeSharesSupply != 0
167✔
302
          ? (_activeStake * expiringTranche.stakeShares) / _stakeSharesSupply
303
          : 0;
304

305
        _activeStake -= expiredStake;
167✔
306
        _stakeSharesSupply -= expiringTranche.stakeShares;
167✔
307
        _rewardsSharesSupply -= expiringTranche.rewardsShares;
167✔
308

309
        emit TrancheExpired(_firstActiveTrancheId);
167✔
310
        // advance to the next tranche
311
        _firstActiveTrancheId++;
167✔
312
      }
313

314
      // end while
315
    }
316

317
    if (updateUntilCurrentTimestamp) {
699✔
318
      uint elapsed = block.timestamp - _lastAccNxmUpdate;
660✔
319
      uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
660✔
320
        ? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
321
        : 0;
322
      _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
660✔
323
      _lastAccNxmUpdate = block.timestamp;
660✔
324
    }
325

326
    firstActiveTrancheId = _firstActiveTrancheId.toUint32();
699✔
327
    firstActiveBucketId = _firstActiveBucketId.toUint32();
699✔
328

329
    activeStake = _activeStake.toUint96();
699✔
330
    rewardPerSecond = _rewardPerSecond.toUint96();
699✔
331
    accNxmPerRewardsShare = _accNxmPerRewardsShare.toUint96();
699✔
332
    lastAccNxmUpdate = _lastAccNxmUpdate.toUint32();
699✔
333
    stakeSharesSupply = _stakeSharesSupply.toUint128();
699✔
334
    rewardsSharesSupply = _rewardsSharesSupply.toUint128();
699✔
335
  }
336

337
  function depositTo(
338
    uint amount,
339
    uint trancheId,
340
    uint requestTokenId,
341
    address destination
342
  ) public whenNotPaused whenNotHalted returns (uint tokenId) {
343

344
    if (isPrivatePool && msg.sender != manager()) {
221✔
345
      revert PrivatePool();
1✔
346
    }
347

348
    if (block.timestamp <= nxm.isLockedForMV(msg.sender) && msg.sender != manager()) {
220✔
349
      revert NxmIsLockedForGovernanceVote();
1✔
350
    }
351

352
    {
219✔
353
      uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
219✔
354
      uint maxTranche = _firstActiveTrancheId + MAX_ACTIVE_TRANCHES - 1;
219✔
355

356
      if (amount == 0) {
219✔
357
        revert InsufficientDepositAmount();
1✔
358
      }
359

360
      if (trancheId > maxTranche) {
218✔
361
        revert RequestedTrancheIsNotYetActive();
1✔
362
      }
363

364
      if (trancheId < _firstActiveTrancheId) {
217✔
365
        revert RequestedTrancheIsExpired();
3✔
366
      }
367

368
      // if the pool has no previous deposits
369
      if (firstActiveTrancheId == 0) {
214✔
370
        firstActiveTrancheId = _firstActiveTrancheId.toUint32();
113✔
371
        firstActiveBucketId = (block.timestamp / BUCKET_DURATION).toUint32();
113✔
372
        lastAccNxmUpdate = block.timestamp.toUint32();
113✔
373
      } else {
374
        processExpirations(true);
101✔
375
      }
376
    }
377

378
    // storage reads
379
    uint _activeStake = activeStake;
214✔
380
    uint _stakeSharesSupply = stakeSharesSupply;
214✔
381
    uint _rewardsSharesSupply = rewardsSharesSupply;
214✔
382
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
214✔
383
    uint totalAmount;
214✔
384

385
    // deposit to token id = 0 is not allowed
386
    // we treat it as a flag to create a new token
387
    if (requestTokenId == 0) {
214✔
388
      address to = destination == address(0) ? msg.sender : destination;
183✔
389
      tokenId = stakingNFT.mint(poolId, to);
183✔
390
    } else {
391
      // validate token id exists and belongs to this pool
392
      // stakingPoolOf() reverts for non-existent tokens
393
      if (stakingNFT.stakingPoolOf(requestTokenId) != poolId) {
31✔
394
        revert InvalidStakingPoolForToken();
1✔
395
      }
396
      // validate only the token owner or an approved address can deposit
397
      if (!stakingNFT.isApprovedOrOwner(msg.sender, requestTokenId)) {
28✔
398
        revert NotTokenOwnerOrApproved();
1✔
399
      }
400

401
      tokenId = requestTokenId;
27✔
402
    }
403

404
    uint newStakeShares = _stakeSharesSupply == 0
210✔
405
      ? Math.sqrt(amount)
406
      : _stakeSharesSupply * amount / _activeStake;
407

408
    uint newRewardsShares;
210✔
409

410
    // update deposit and pending reward
411
    {
210✔
412
      // conditional read
413
      Deposit memory deposit = requestTokenId == 0
210✔
414
        ? Deposit(0, 0, 0, 0)
415
        : deposits[tokenId][trancheId];
416

417
      newRewardsShares = calculateNewRewardShares(
210✔
418
        deposit.stakeShares, // initialStakeShares
419
        newStakeShares, // newStakeShares
420
        trancheId, // initialTrancheId
421
        trancheId, // newTrancheId, the same as initialTrancheId in this case
422
        block.timestamp
423
      );
424

425
      // if we're increasing an existing deposit
426
      if (deposit.rewardsShares != 0) {
210✔
427
        uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(deposit.lastAccNxmPerRewardShare);
3✔
428
        deposit.pendingRewards += (newEarningsPerShare * deposit.rewardsShares / ONE_NXM).toUint96();
3✔
429
      }
430

431
      deposit.stakeShares += newStakeShares.toUint128();
210✔
432
      deposit.rewardsShares += newRewardsShares.toUint128();
210✔
433
      deposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
210✔
434

435
      // store
436
      deposits[tokenId][trancheId] = deposit;
210✔
437
    }
438

439
    // update pool manager's reward shares
440
    {
210✔
441
      Deposit memory feeDeposit = deposits[0][trancheId];
210✔
442

443
      {
210✔
444
        // create fee deposit reward shares
445
        uint newFeeRewardShares = newRewardsShares * poolFee / (POOL_FEE_DENOMINATOR - poolFee);
210✔
446
        newRewardsShares += newFeeRewardShares;
210✔
447

448
        // calculate rewards until now
449
        uint newRewardPerShare = _accNxmPerRewardsShare.uncheckedSub(feeDeposit.lastAccNxmPerRewardShare);
210✔
450
        feeDeposit.pendingRewards += (newRewardPerShare * feeDeposit.rewardsShares / ONE_NXM).toUint96();
210✔
451
        feeDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
210✔
452
        feeDeposit.rewardsShares += newFeeRewardShares.toUint128();
210✔
453
      }
454

455
      deposits[0][trancheId] = feeDeposit;
210✔
456
    }
457

458
    // update tranche
459
    {
210✔
460
      Tranche memory tranche = tranches[trancheId];
210✔
461
      tranche.stakeShares += newStakeShares.toUint128();
210✔
462
      tranche.rewardsShares += newRewardsShares.toUint128();
210✔
463
      tranches[trancheId] = tranche;
210✔
464
    }
465

466
    totalAmount += amount;
210✔
467
    _activeStake += amount;
210✔
468
    _stakeSharesSupply += newStakeShares;
210✔
469
    _rewardsSharesSupply += newRewardsShares;
210✔
470

471
    // transfer nxm from the staker and update the pool deposit balance
472
    tokenController.depositStakedNXM(msg.sender, totalAmount, poolId);
210✔
473

474
    // update globals
475
    activeStake = _activeStake.toUint96();
210✔
476
    stakeSharesSupply = _stakeSharesSupply.toUint128();
210✔
477
    rewardsSharesSupply = _rewardsSharesSupply.toUint128();
210✔
478

479
    emit StakeDeposited(msg.sender, amount, trancheId, tokenId);
210✔
480
  }
481

482
  function getTimeLeftOfTranche(uint trancheId, uint blockTimestamp) internal pure returns (uint) {
483
    uint endDate = (trancheId + 1) * TRANCHE_DURATION;
538✔
484
    return endDate > blockTimestamp ? endDate - blockTimestamp : 0;
538✔
485
  }
486

487
  /// Calculates the amount of new reward shares based on the initial and new stake shares
488
  ///
489
  /// @param initialStakeShares   Amount of stake shares the deposit is already entitled to
490
  /// @param stakeSharesIncrease  Amount of additional stake shares the deposit will be entitled to
491
  /// @param initialTrancheId     The id of the initial tranche that defines the deposit period
492
  /// @param newTrancheId         The new id of the tranche that will define the deposit period
493
  /// @param blockTimestamp       The timestamp of the block when the new shares are recalculated
494
  function calculateNewRewardShares(
495
    uint initialStakeShares,
496
    uint stakeSharesIncrease,
497
    uint initialTrancheId,
498
    uint newTrancheId,
499
    uint blockTimestamp
500
  ) public pure returns (uint) {
501

502
    uint timeLeftOfInitialTranche = getTimeLeftOfTranche(initialTrancheId, blockTimestamp);
269✔
503
    uint timeLeftOfNewTranche = getTimeLeftOfTranche(newTrancheId, blockTimestamp);
269✔
504

505
    // the bonus is based on the the time left and the total amount of stake shares (initial + new)
506
    uint newBonusShares = (initialStakeShares + stakeSharesIncrease)
269✔
507
      * REWARD_BONUS_PER_TRANCHE_RATIO
508
      * timeLeftOfNewTranche
509
      / TRANCHE_DURATION
510
      / REWARD_BONUS_PER_TRANCHE_DENOMINATOR;
511

512
    // for existing deposits, the previous bonus is deducted from the final amount
513
    uint previousBonusSharesDeduction = initialStakeShares
269✔
514
      * REWARD_BONUS_PER_TRANCHE_RATIO
515
      * timeLeftOfInitialTranche
516
      / TRANCHE_DURATION
517
      / REWARD_BONUS_PER_TRANCHE_DENOMINATOR;
518

519
    return stakeSharesIncrease + newBonusShares - previousBonusSharesDeduction;
269✔
520
  }
521

522
  function withdraw(
523
    uint tokenId,
524
    bool withdrawStake,
525
    bool withdrawRewards,
526
    uint[] memory trancheIds
527
  ) public whenNotPaused returns (uint withdrawnStake, uint withdrawnRewards) {
528

529
    uint managerLockedInGovernanceUntil = nxm.isLockedForMV(manager());
23✔
530

531
    // pass false as it does not modify the share supply nor the reward per second
532
    processExpirations(true);
23✔
533

534
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
23✔
535
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
23✔
536
    uint trancheCount = trancheIds.length;
23✔
537

538
    for (uint j = 0; j < trancheCount; j++) {
23✔
539

540
      uint trancheId = trancheIds[j];
45✔
541

542
      Deposit memory deposit = deposits[tokenId][trancheId];
45✔
543

544
      {
45✔
545
        uint trancheRewardsToWithdraw;
45✔
546
        uint trancheStakeToWithdraw;
45✔
547

548
        // can withdraw stake only if the tranche is expired
549
        if (withdrawStake && trancheId < _firstActiveTrancheId) {
45✔
550

551
          // Deposit withdrawals are not permitted while the manager is locked in governance to
552
          // prevent double voting.
553
          if (managerLockedInGovernanceUntil > block.timestamp) {
37✔
554
            revert ManagerNxmIsLockedForGovernanceVote();
1✔
555
          }
556

557
          // calculate the amount of nxm for this deposit
558
          uint stake = expiredTranches[trancheId].stakeAmountAtExpiry;
36✔
559
          uint _stakeSharesSupply = expiredTranches[trancheId].stakeSharesSupplyAtExpiry;
36✔
560
          trancheStakeToWithdraw = stake * deposit.stakeShares / _stakeSharesSupply;
36✔
561
          withdrawnStake += trancheStakeToWithdraw;
36✔
562

563
          // mark as withdrawn
564
          deposit.stakeShares = 0;
36✔
565
        }
566

567
        if (withdrawRewards) {
44✔
568

569
          // if the tranche is expired, use the accumulator value saved at expiration time
570
          uint accNxmPerRewardShareToUse = trancheId < _firstActiveTrancheId
42✔
571
            ? expiredTranches[trancheId].accNxmPerRewardShareAtExpiry
572
            : _accNxmPerRewardsShare;
573

574
          // calculate reward since checkpoint
575
          uint newRewardPerShare = accNxmPerRewardShareToUse.uncheckedSub(deposit.lastAccNxmPerRewardShare);
42✔
576
          trancheRewardsToWithdraw = newRewardPerShare * deposit.rewardsShares / ONE_NXM + deposit.pendingRewards;
42✔
577
          withdrawnRewards += trancheRewardsToWithdraw;
42✔
578

579
          // save checkpoint
580
          deposit.lastAccNxmPerRewardShare = accNxmPerRewardShareToUse.toUint96();
42✔
581
          deposit.pendingRewards = 0;
42✔
582
        }
583

584
        emit Withdraw(msg.sender, tokenId, trancheId, trancheStakeToWithdraw, trancheRewardsToWithdraw);
44✔
585
      }
586

587
      deposits[tokenId][trancheId] = deposit;
44✔
588
    }
589

590
    address destination = tokenId == 0
22✔
591
      ? manager()
592
      : stakingNFT.ownerOf(tokenId);
593

594
    tokenController.withdrawNXMStakeAndRewards(
22✔
595
      destination,
596
      withdrawnStake,
597
      withdrawnRewards,
598
      poolId
599
    );
600

601
    return (withdrawnStake, withdrawnRewards);
22✔
602
  }
603

604
  function requestAllocation(
605
    uint amount,
606
    uint previousPremium,
607
    AllocationRequest calldata request
608
  ) external onlyCoverContract returns (uint premium, uint allocationId) {
609

610
    // passing true because we change the reward per second
611
    processExpirations(true);
520✔
612

613
    // prevent allocation requests (edits and forced expirations) for expired covers
614
    if (request.allocationId != 0) {
520✔
615
      uint expirationBucketId = Math.divCeil(request.previousExpiration, BUCKET_DURATION);
19✔
616
      if (coverTrancheAllocations[request.allocationId] == 0 || firstActiveBucketId >= expirationBucketId) {
19✔
617
        revert AlreadyDeallocated(request.allocationId);
3✔
618
      }
619
    }
620

621
    uint[] memory trancheAllocations = request.allocationId == 0
517✔
622
      ? getActiveAllocations(request.productId)
623
      : getActiveAllocationsWithoutCover(
624
          request.productId,
625
          request.allocationId,
626
          request.previousStart,
627
          request.previousExpiration
628
        );
629

630
    // we are only deallocating
631
    // rewards streaming is left as is
632
    if (amount == 0) {
517✔
633
      // store deallocated amount
634
      updateStoredAllocations(
6✔
635
        request.productId,
636
        block.timestamp / TRANCHE_DURATION, // firstActiveTrancheId
637
        trancheAllocations
638
      );
639

640
      // update coverTrancheAllocations when deallocating so we can track deallocation
641
      delete coverTrancheAllocations[request.allocationId];
6✔
642
      emit Deallocated(request.allocationId);
6✔
643
      return (0, 0);
6✔
644
    }
645

646
    uint coverAllocationAmount;
511✔
647
    uint initialCapacityUsed;
511✔
648
    uint totalCapacity;
511✔
649
    (
511✔
650
      coverAllocationAmount,
651
      initialCapacityUsed,
652
      totalCapacity,
653
      allocationId
654
    ) = allocate(amount, request, trancheAllocations);
655

656
    // the returned premium value has 18 decimals
657
    premium = stakingProducts.getPremium(
498✔
658
      poolId,
659
      request.productId,
660
      request.period,
661
      coverAllocationAmount,
662
      initialCapacityUsed,
663
      totalCapacity,
664
      request.globalMinPrice,
665
      request.useFixedPrice,
666
      NXM_PER_ALLOCATION_UNIT,
667
      ALLOCATION_UNITS_PER_NXM
668
    );
669

670
    // add new rewards
671
    {
498✔
672
      if (request.rewardRatio > REWARDS_DENOMINATOR) {
498!
673
        revert RewardRatioTooHigh();
×
674
      }
675

676
      uint expirationBucket = Math.divCeil(block.timestamp + request.period, BUCKET_DURATION);
498✔
677
      uint rewardStreamPeriod = expirationBucket * BUCKET_DURATION - block.timestamp;
498✔
678
      uint _rewardPerSecond = (premium * request.rewardRatio / REWARDS_DENOMINATOR) / rewardStreamPeriod;
498✔
679

680
      // store
681
      rewardPerSecondCut[expirationBucket] += _rewardPerSecond;
498✔
682
      rewardPerSecond += _rewardPerSecond.toUint96();
498✔
683

684
      uint rewardsToMint = _rewardPerSecond * rewardStreamPeriod;
498✔
685
      tokenController.mintStakingPoolNXMRewards(rewardsToMint, poolId);
498✔
686
    }
687

688
    // remove previous rewards
689
    if (previousPremium > 0) {
498✔
690

691
      uint prevRewards = previousPremium * request.previousRewardsRatio / REWARDS_DENOMINATOR;
1✔
692
      uint prevExpirationBucket = Math.divCeil(request.previousExpiration, BUCKET_DURATION);
1✔
693
      uint rewardStreamPeriod = prevExpirationBucket * BUCKET_DURATION - request.previousStart;
1✔
694
      uint prevRewardsPerSecond = prevRewards / rewardStreamPeriod;
1✔
695

696
      // store
697
      rewardPerSecondCut[prevExpirationBucket] -= prevRewardsPerSecond;
1✔
698
      rewardPerSecond -= prevRewardsPerSecond.toUint96();
1✔
699

700
      // prevRewardsPerSecond * rewardStreamPeriodLeft
701
      uint rewardsToBurn = prevRewardsPerSecond * (prevExpirationBucket * BUCKET_DURATION - block.timestamp);
1✔
702
      tokenController.burnStakingPoolNXMRewards(rewardsToBurn, poolId);
1✔
703
    }
704

705
    return (premium, allocationId);
498✔
706
  }
707

708
  function getActiveAllocationsWithoutCover(
709
    uint productId,
710
    uint allocationId,
711
    uint start,
712
    uint expiration
713
  ) internal returns (uint[] memory activeAllocations) {
714

715
    uint packedCoverTrancheAllocation = coverTrancheAllocations[allocationId];
16✔
716
    activeAllocations = getActiveAllocations(productId);
16✔
717

718
    uint currentFirstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
16✔
719
    uint[] memory coverAllocations = new uint[](MAX_ACTIVE_TRANCHES);
16✔
720

721
    // number of already expired tranches to skip
722
    // currentFirstActiveTranche - previousFirstActiveTranche
723
    uint offset = currentFirstActiveTrancheId - (start / TRANCHE_DURATION);
16✔
724

725
    for (uint i = offset; i < MAX_ACTIVE_TRANCHES; i++) {
16✔
726
      uint allocated = uint32(packedCoverTrancheAllocation >> (i * 32));
124✔
727
      uint currentTrancheIdx = i - offset;
124✔
728
      activeAllocations[currentTrancheIdx] -= allocated;
124✔
729
      coverAllocations[currentTrancheIdx] = allocated;
124✔
730
    }
731

732
    // remove expiring cover amounts from buckets
733
    updateExpiringCoverAmounts(
16✔
734
      productId,
735
      currentFirstActiveTrancheId,
736
      Math.divCeil(expiration, BUCKET_DURATION), // targetBucketId
737
      coverAllocations,
738
      false // isAllocation
739
    );
740

741
    return activeAllocations;
16✔
742
  }
743

744
  function getActiveAllocations(
745
    uint productId
746
  ) public view returns (uint[] memory trancheAllocations) {
747

748
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
1,564✔
749
    uint currentBucket = block.timestamp / BUCKET_DURATION;
1,564✔
750
    uint lastBucketId;
1,564✔
751

752
    (trancheAllocations, lastBucketId) = getStoredAllocations(productId, _firstActiveTrancheId);
1,564✔
753

754
    if (lastBucketId == 0) {
1,564✔
755
      lastBucketId = currentBucket;
800✔
756
    }
757

758
    for (uint bucketId = lastBucketId + 1; bucketId <= currentBucket; bucketId++) {
1,564✔
759

760
      uint[] memory expirations = getExpiringCoverAmounts(productId, bucketId, _firstActiveTrancheId);
87✔
761

762
      for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
87✔
763
        trancheAllocations[i] -= expirations[i];
696✔
764
      }
765
    }
766

767
    return trancheAllocations;
1,564✔
768
  }
769

770
  function getStoredAllocations(
771
    uint productId,
772
    uint firstTrancheId
773
  ) internal view returns (
774
    uint[] memory storedAllocations,
775
    uint16 lastBucketId
776
  ) {
777

778
    storedAllocations = new uint[](MAX_ACTIVE_TRANCHES);
1,564✔
779

780
    uint firstGroupId = firstTrancheId / COVER_TRANCHE_GROUP_SIZE;
1,564✔
781
    uint lastGroupId = (firstTrancheId + MAX_ACTIVE_TRANCHES - 1) / COVER_TRANCHE_GROUP_SIZE;
1,564✔
782

783
    // min 2 and max 3 groups
784
    uint groupCount = lastGroupId - firstGroupId + 1;
1,564✔
785

786
    TrancheAllocationGroup[] memory allocationGroups = new TrancheAllocationGroup[](groupCount);
1,564✔
787

788
    for (uint i = 0; i < groupCount; i++) {
1,564✔
789
      allocationGroups[i] = trancheAllocationGroups[productId][firstGroupId + i];
3,313✔
790
    }
791

792
    lastBucketId = allocationGroups[0].getLastBucketId();
1,564✔
793

794
    // flatten groups
795
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
1,564✔
796
      uint trancheId = firstTrancheId + i;
12,512✔
797
      uint trancheGroupIndex = trancheId / COVER_TRANCHE_GROUP_SIZE - firstGroupId;
12,512✔
798
      uint trancheIndexInGroup = trancheId % COVER_TRANCHE_GROUP_SIZE;
12,512✔
799
      storedAllocations[i] = allocationGroups[trancheGroupIndex].getItemAt(trancheIndexInGroup);
12,512✔
800
    }
801
  }
802

803
  function getExpiringCoverAmounts(
804
    uint productId,
805
    uint bucketId,
806
    uint firstTrancheId
807
  ) internal view returns (uint[] memory expiringCoverAmounts) {
808

809
    expiringCoverAmounts = new uint[](MAX_ACTIVE_TRANCHES);
87✔
810

811
    uint firstGroupId = firstTrancheId / BUCKET_TRANCHE_GROUP_SIZE;
87✔
812
    uint lastGroupId = (firstTrancheId + MAX_ACTIVE_TRANCHES - 1) / BUCKET_TRANCHE_GROUP_SIZE;
87✔
813

814
    // min 1, max 2
815
    uint groupCount = lastGroupId - firstGroupId + 1;
87✔
816
    TrancheGroupBucket[] memory trancheGroupBuckets = new TrancheGroupBucket[](groupCount);
87✔
817

818
    // min 1 and max 2 reads
819
    for (uint i = 0; i < groupCount; i++) {
87✔
820
      trancheGroupBuckets[i] = expiringCoverBuckets[productId][bucketId][firstGroupId + i];
170✔
821
    }
822

823
    // flatten bucket tranche groups
824
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
87✔
825
      uint trancheId = firstTrancheId + i;
696✔
826
      uint trancheGroupIndex = trancheId / BUCKET_TRANCHE_GROUP_SIZE - firstGroupId;
696✔
827
      uint trancheIndexInGroup = trancheId % BUCKET_TRANCHE_GROUP_SIZE;
696✔
828
      expiringCoverAmounts[i] = trancheGroupBuckets[trancheGroupIndex].getItemAt(trancheIndexInGroup);
696✔
829
    }
830

831
    return expiringCoverAmounts;
87✔
832
  }
833

834
  function getActiveTrancheCapacities(
835
    uint productId,
836
    uint globalCapacityRatio,
837
    uint capacityReductionRatio
838
  ) public view returns (
839
    uint[] memory trancheCapacities,
840
    uint totalCapacity
841
  ) {
842

843
    trancheCapacities = getTrancheCapacities(
5✔
844
      productId,
845
      block.timestamp / TRANCHE_DURATION, // first active tranche id
846
      MAX_ACTIVE_TRANCHES,
847
      globalCapacityRatio,
848
      capacityReductionRatio
849
    );
850

851
    totalCapacity = Math.sum(trancheCapacities);
5✔
852

853
    return (trancheCapacities, totalCapacity);
5✔
854
  }
855

856
  function getTrancheCapacities(
857
    uint productId,
858
    uint firstTrancheId,
859
    uint trancheCount,
860
    uint capacityRatio,
861
    uint reductionRatio
862
  ) public view returns (uint[] memory trancheCapacities) {
863

864
    // will revert if with unprocessed expirations
865
    if (firstTrancheId < block.timestamp / TRANCHE_DURATION) {
516!
866
      revert RequestedTrancheIsExpired();
×
867
    }
868

869
    uint _activeStake = activeStake;
516✔
870
    uint _stakeSharesSupply = stakeSharesSupply;
516✔
871
    trancheCapacities = new uint[](trancheCount);
516✔
872

873
    if (_stakeSharesSupply == 0) {
516!
874
      return trancheCapacities;
×
875
    }
876

877
    // TODO: can we get rid of the extra call to SP here?
878
    uint multiplier =
516✔
879
      capacityRatio
880
      * (CAPACITY_REDUCTION_DENOMINATOR - reductionRatio)
881
      * stakingProducts.getProductTargetWeight(poolId, productId);
882

883
    uint denominator =
516✔
884
      GLOBAL_CAPACITY_DENOMINATOR
885
      * CAPACITY_REDUCTION_DENOMINATOR
886
      * WEIGHT_DENOMINATOR;
887

888
    for (uint i = 0; i < trancheCount; i++) {
516✔
889
      uint trancheStake = (_activeStake * tranches[firstTrancheId + i].stakeShares / _stakeSharesSupply);
4,128✔
890
      trancheCapacities[i] = trancheStake * multiplier / denominator / NXM_PER_ALLOCATION_UNIT;
4,128✔
891
    }
892

893
    return trancheCapacities;
516✔
894
  }
895

896
  function allocate(
897
    uint amount,
898
    AllocationRequest calldata request,
899
    uint[] memory trancheAllocations
900
  ) internal returns (
901
    uint coverAllocationAmount,
902
    uint initialCapacityUsed,
903
    uint totalCapacity,
904
    uint allocationId
905
  ) {
906

907
    if (request.allocationId == 0) {
511✔
908
      allocationId = ++lastAllocationId;
501✔
909
    } else {
910
      allocationId = request.allocationId;
10✔
911
    }
912

913
    coverAllocationAmount = Math.divCeil(amount, NXM_PER_ALLOCATION_UNIT);
511✔
914

915
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
511✔
916
    uint[] memory coverAllocations = new uint[](MAX_ACTIVE_TRANCHES);
511✔
917

918
    {
511✔
919
      uint firstTrancheIdToUse = (block.timestamp + request.period + request.gracePeriod) / TRANCHE_DURATION;
511✔
920
      uint startIndex = firstTrancheIdToUse - _firstActiveTrancheId;
511✔
921

922
      uint[] memory trancheCapacities = getTrancheCapacities(
511✔
923
        request.productId,
924
        _firstActiveTrancheId,
925
        MAX_ACTIVE_TRANCHES, // count
926
        request.globalCapacityRatio,
927
        request.capacityReductionRatio
928
      );
929

930
      uint remainingAmount = coverAllocationAmount;
511✔
931
      uint carryOver;
511✔
932
      uint packedCoverAllocations;
511✔
933

934
      for (uint i = 0; i < startIndex; i++) {
511✔
935

936
        uint allocated = trancheAllocations[i];
145✔
937
        uint capacity = trancheCapacities[i];
145✔
938

939
        if (allocated > capacity) {
145✔
940
          carryOver += allocated - capacity;
3✔
941
        } else if (carryOver > 0) {
142!
942
          carryOver -= Math.min(carryOver, capacity - allocated);
×
943
        }
944
      }
945

946
      initialCapacityUsed = carryOver;
511✔
947

948
      for (uint i = startIndex; i < MAX_ACTIVE_TRANCHES; i++) {
511✔
949

950
        initialCapacityUsed += trancheAllocations[i];
3,943✔
951
        totalCapacity += trancheCapacities[i];
3,943✔
952

953
        if (trancheAllocations[i] >= trancheCapacities[i]) {
3,943✔
954
          // carry over overallocation
955
          carryOver += trancheAllocations[i] - trancheCapacities[i];
3,348✔
956
          continue;
3,348✔
957
        }
958

959
        if (remainingAmount == 0) {
595✔
960
          // not breaking out of the for loop because we need the total capacity calculated above
961
          continue;
57✔
962
        }
963

964
        uint allocatedAmount;
538✔
965

966
        {
538✔
967
          uint available = trancheCapacities[i] - trancheAllocations[i];
538✔
968

969
          if (carryOver > available) {
538!
970
            // no capacity left in this tranche
971
            carryOver -= available;
×
972
            continue;
×
973
          }
974

975
          available -= carryOver;
538✔
976
          carryOver = 0;
538✔
977
          allocatedAmount = Math.min(available, remainingAmount);
538✔
978
        }
979

980
        coverAllocations[i] = allocatedAmount;
538✔
981
        trancheAllocations[i] += allocatedAmount;
538✔
982
        remainingAmount -= allocatedAmount;
538✔
983

984
        packedCoverAllocations |= allocatedAmount << i * 32;
538✔
985
      }
986

987
      coverTrancheAllocations[allocationId] = packedCoverAllocations;
511✔
988

989
      if (remainingAmount != 0) {
511✔
990
        revert InsufficientCapacity();
12✔
991
      }
992
    }
993

994
    updateExpiringCoverAmounts(
499✔
995
      request.productId,
996
      _firstActiveTrancheId,
997
      Math.divCeil(block.timestamp + request.period, BUCKET_DURATION), // targetBucketId
998
      coverAllocations,
999
      true // isAllocation
1000
    );
1001

1002
    updateStoredAllocations(
498✔
1003
      request.productId,
1004
      _firstActiveTrancheId,
1005
      trancheAllocations
1006
    );
1007

1008
    return (coverAllocationAmount, initialCapacityUsed, totalCapacity, allocationId);
498✔
1009
  }
1010

1011
  function updateStoredAllocations(
1012
    uint productId,
1013
    uint firstTrancheId,
1014
    uint[] memory allocations
1015
  ) internal {
1016

1017
    uint firstGroupId = firstTrancheId / COVER_TRANCHE_GROUP_SIZE;
561✔
1018
    uint lastGroupId = (firstTrancheId + MAX_ACTIVE_TRANCHES - 1) / COVER_TRANCHE_GROUP_SIZE;
561✔
1019
    uint groupCount = lastGroupId - firstGroupId + 1;
561✔
1020

1021
    TrancheAllocationGroup[] memory allocationGroups = new TrancheAllocationGroup[](groupCount);
561✔
1022

1023
    // min 2 and max 3 reads
1024
    for (uint i = 0; i < groupCount; i++) {
561✔
1025
      allocationGroups[i] = trancheAllocationGroups[productId][firstGroupId + i];
1,247✔
1026
    }
1027

1028
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
561✔
1029

1030
      uint trancheId = firstTrancheId + i;
4,488✔
1031
      uint trancheGroupIndex = trancheId / COVER_TRANCHE_GROUP_SIZE - firstGroupId;
4,488✔
1032
      uint trancheIndexInGroup = trancheId % COVER_TRANCHE_GROUP_SIZE;
4,488✔
1033

1034
      // setItemAt does not mutate so we have to reassign it
1035
      allocationGroups[trancheGroupIndex] = allocationGroups[trancheGroupIndex].setItemAt(
4,488✔
1036
        trancheIndexInGroup,
1037
        allocations[i].toUint48()
1038
      );
1039
    }
1040

1041
    uint16 currentBucket = (block.timestamp / BUCKET_DURATION).toUint16();
561✔
1042

1043
    for (uint i = 0; i < groupCount; i++) {
561✔
1044
      trancheAllocationGroups[productId][firstGroupId + i] = allocationGroups[i].setLastBucketId(currentBucket);
1,247✔
1045
    }
1046
  }
1047

1048
  function updateExpiringCoverAmounts(
1049
    uint productId,
1050
    uint firstTrancheId,
1051
    uint targetBucketId,
1052
    uint[] memory coverTrancheAllocation,
1053
    bool isAllocation
1054
  ) internal {
1055

1056
    uint firstGroupId = firstTrancheId / BUCKET_TRANCHE_GROUP_SIZE;
572✔
1057
    uint lastGroupId = (firstTrancheId + MAX_ACTIVE_TRANCHES - 1) / BUCKET_TRANCHE_GROUP_SIZE;
572✔
1058
    uint groupCount = lastGroupId - firstGroupId + 1;
572✔
1059

1060
    TrancheGroupBucket[] memory trancheGroupBuckets = new TrancheGroupBucket[](groupCount);
572✔
1061

1062
    // min 1 and max 2 reads
1063
    for (uint i = 0; i < groupCount; i++) {
572✔
1064
      trancheGroupBuckets[i] = expiringCoverBuckets[productId][targetBucketId][firstGroupId + i];
1,143✔
1065
    }
1066

1067
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
572✔
1068

1069
      uint trancheId = firstTrancheId + i;
4,572✔
1070
      uint trancheGroupId = trancheId / BUCKET_TRANCHE_GROUP_SIZE - firstGroupId;
4,572✔
1071
      uint trancheIndexInGroup = trancheId % BUCKET_TRANCHE_GROUP_SIZE;
4,572✔
1072

1073
      uint32 expiringAmount = trancheGroupBuckets[trancheGroupId].getItemAt(trancheIndexInGroup);
4,572✔
1074
      uint32 trancheAllocation = coverTrancheAllocation[i].toUint32();
4,572✔
1075

1076
      if (isAllocation) {
4,571✔
1077
        expiringAmount += trancheAllocation;
3,987✔
1078
      } else {
1079
        expiringAmount -= trancheAllocation;
584✔
1080
      }
1081

1082
      // setItemAt does not mutate so we have to reassign it
1083
      trancheGroupBuckets[trancheGroupId] = trancheGroupBuckets[trancheGroupId].setItemAt(
4,571✔
1084
        trancheIndexInGroup,
1085
        expiringAmount
1086
      );
1087
    }
1088

1089
    for (uint i = 0; i < groupCount; i++) {
571✔
1090
      expiringCoverBuckets[productId][targetBucketId][firstGroupId + i] = trancheGroupBuckets[i];
1,141✔
1091
    }
1092
  }
1093

1094
  /// Extends the period of an existing deposit until a tranche that ends further into the future
1095
  ///
1096
  /// @param tokenId           The id of the NFT that proves the ownership of the deposit.
1097
  /// @param initialTrancheId  The id of the tranche the deposit is already a part of.
1098
  /// @param newTrancheId      The id of the new tranche determining the new deposit period.
1099
  /// @param topUpAmount       An optional amount if the user wants to also increase the deposit
1100
  function extendDeposit(
1101
    uint tokenId,
1102
    uint initialTrancheId,
1103
    uint newTrancheId,
1104
    uint topUpAmount
1105
  ) external whenNotPaused whenNotHalted {
1106

1107
    // token id 0 is only used for pool manager fee tracking, no deposits allowed
1108
    if (tokenId == 0) {
22✔
1109
      revert InvalidTokenId();
1✔
1110
    }
1111

1112
    // validate token id exists and belongs to this pool
1113
    // stakingPoolOf() reverts for non-existent tokens
1114
    if (stakingNFT.stakingPoolOf(tokenId) != poolId) {
21!
1115
      revert InvalidStakingPoolForToken();
×
1116
    }
1117

1118
    if (isPrivatePool && msg.sender != manager()) {
21✔
1119
      revert PrivatePool();
2✔
1120
    }
1121

1122
    if (!stakingNFT.isApprovedOrOwner(msg.sender, tokenId)) {
19✔
1123
      revert NotTokenOwnerOrApproved();
1✔
1124
    }
1125

1126
    if (topUpAmount > 0 && block.timestamp <= nxm.isLockedForMV(msg.sender)) {
18✔
1127
      revert NxmIsLockedForGovernanceVote();
1✔
1128
    }
1129

1130
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
17✔
1131

1132
    {
17✔
1133
      if (initialTrancheId >= newTrancheId) {
17✔
1134
        revert NewTrancheEndsBeforeInitialTranche();
1✔
1135
      }
1136

1137
      uint maxTrancheId = _firstActiveTrancheId + MAX_ACTIVE_TRANCHES - 1;
16✔
1138

1139
      if (newTrancheId > maxTrancheId) {
16✔
1140
        revert RequestedTrancheIsNotYetActive();
1✔
1141
      }
1142

1143
      if (newTrancheId < firstActiveTrancheId) {
15!
1144
        revert RequestedTrancheIsExpired();
×
1145
      }
1146
    }
1147

1148
    // if the initial tranche is expired, withdraw everything and make a new deposit
1149
    // this requires the user to have grante sufficient allowance
1150
    if (initialTrancheId < _firstActiveTrancheId) {
15✔
1151

1152
      uint[] memory trancheIds = new uint[](1);
5✔
1153
      trancheIds[0] = initialTrancheId;
5✔
1154

1155
      (uint withdrawnStake, /* uint rewardsToWithdraw */) = withdraw(
5✔
1156
        tokenId,
1157
        true, // withdraw the deposit
1158
        true, // withdraw the rewards
1159
        trancheIds
1160
      );
1161

1162
      depositTo(withdrawnStake + topUpAmount, newTrancheId, tokenId, msg.sender);
5✔
1163

1164
      return;
4✔
1165
      // done! skip the rest of the function.
1166
    }
1167

1168
    // if we got here - the initial tranche is still active. move all the shares to the new tranche
1169

1170
    // passing true because we mint reward shares
1171
    processExpirations(true);
10✔
1172

1173
    Deposit memory initialDeposit = deposits[tokenId][initialTrancheId];
10✔
1174
    Deposit memory updatedDeposit = deposits[tokenId][newTrancheId];
10✔
1175

1176
    uint _activeStake = activeStake;
10✔
1177
    uint _stakeSharesSupply = stakeSharesSupply;
10✔
1178
    uint newStakeShares;
10✔
1179

1180
    // calculate the new stake shares if there's a deposit top up
1181
    if (topUpAmount > 0) {
10✔
1182
      newStakeShares = _stakeSharesSupply * topUpAmount / _activeStake;
5✔
1183
      activeStake = (_activeStake + topUpAmount).toUint96();
5✔
1184
    }
1185

1186
    // calculate the new reward shares
1187
    uint newRewardsShares = calculateNewRewardShares(
10✔
1188
      initialDeposit.stakeShares,
1189
      newStakeShares,
1190
      initialTrancheId,
1191
      newTrancheId,
1192
      block.timestamp
1193
    );
1194

1195
    {
10✔
1196
      Tranche memory initialTranche = tranches[initialTrancheId];
10✔
1197
      Tranche memory newTranche = tranches[newTrancheId];
10✔
1198

1199
      // move the shares to the new tranche
1200
      initialTranche.stakeShares -= initialDeposit.stakeShares;
10✔
1201
      initialTranche.rewardsShares -= initialDeposit.rewardsShares;
10✔
1202
      newTranche.stakeShares += initialDeposit.stakeShares + newStakeShares.toUint128();
10✔
1203
      newTranche.rewardsShares += (initialDeposit.rewardsShares + newRewardsShares).toUint128();
10✔
1204

1205
      // store the updated tranches
1206
      tranches[initialTrancheId] = initialTranche;
10✔
1207
      tranches[newTrancheId] = newTranche;
10✔
1208
    }
1209

1210
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
10✔
1211

1212
    // if there already is a deposit on the new tranche, calculate its pending rewards
1213
    if (updatedDeposit.lastAccNxmPerRewardShare != 0) {
10!
1214
      uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(updatedDeposit.lastAccNxmPerRewardShare);
×
1215
      updatedDeposit.pendingRewards += (newEarningsPerShare * updatedDeposit.rewardsShares / ONE_NXM).toUint96();
×
1216
    }
1217

1218
    // calculate the rewards for the deposit being extended and move them to the new deposit
1219
    {
10✔
1220
      uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(initialDeposit.lastAccNxmPerRewardShare);
10✔
1221
      updatedDeposit.pendingRewards += (newEarningsPerShare * initialDeposit.rewardsShares / ONE_NXM).toUint96();
10✔
1222
      updatedDeposit.pendingRewards += initialDeposit.pendingRewards;
10✔
1223
    }
1224

1225
    updatedDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
10✔
1226
    updatedDeposit.stakeShares += (initialDeposit.stakeShares + newStakeShares).toUint128();
10✔
1227
    updatedDeposit.rewardsShares += (initialDeposit.rewardsShares + newRewardsShares).toUint128();
10✔
1228

1229
    // everything is moved, delete the initial deposit
1230
    delete deposits[tokenId][initialTrancheId];
10✔
1231

1232
    // store the new deposit.
1233
    deposits[tokenId][newTrancheId] = updatedDeposit;
10✔
1234

1235
    // update global shares supply
1236
    stakeSharesSupply = (_stakeSharesSupply + newStakeShares).toUint128();
10✔
1237
    rewardsSharesSupply += newRewardsShares.toUint128();
10✔
1238

1239
    // transfer nxm from the staker and update the pool deposit balance
1240
    tokenController.depositStakedNXM(msg.sender, topUpAmount, poolId);
10✔
1241

1242
    emit DepositExtended(msg.sender, tokenId, initialTrancheId, newTrancheId, topUpAmount);
10✔
1243
  }
1244

1245
  function burnStake(uint amount, BurnStakeParams calldata params) external onlyCoverContract {
1246
    // passing false because neither the amount of shares nor the reward per second are changed
1247
    processExpirations(false);
57✔
1248

1249
    // sload
1250
    uint _activeStake = activeStake;
57✔
1251

1252
    // If all stake is burned, leave 1 wei and close pool
1253
    if (amount >= _activeStake) {
57✔
1254
      amount = _activeStake - 1;
3✔
1255
      isHalted = true;
3✔
1256
    }
1257

1258
    tokenController.burnStakedNXM(amount, poolId);
57✔
1259

1260
    // sstore
1261
    activeStake = (_activeStake - amount).toUint96();
57✔
1262

1263
    uint initialPackedCoverTrancheAllocation = coverTrancheAllocations[params.allocationId];
57✔
1264
    uint[] memory activeAllocations = getActiveAllocations(params.productId);
57✔
1265

1266
    uint currentFirstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
57✔
1267
    uint[] memory coverDeallocations = new uint[](MAX_ACTIVE_TRANCHES);
57✔
1268

1269
    uint remainingDeallocationAmount = params.deallocationAmount / NXM_PER_ALLOCATION_UNIT;
57✔
1270
    uint newPackedCoverAllocations;
57✔
1271

1272
    // number of already expired tranches to skip
1273
    // currentFirstActiveTranche - previousFirstActiveTranche
1274
    uint offset = currentFirstActiveTrancheId - (params.start / TRANCHE_DURATION);
57✔
1275

1276
    // iterate the tranches backward to remove allocation from future tranches first
1277
    for (uint i = MAX_ACTIVE_TRANCHES - 1; i >= offset; i--) {
57✔
1278
      // i = tranche index when the allocation was made
1279
      // i - offset = index of the same tranche but in currently active tranches arrays
1280
      uint currentTrancheIdx = i - offset;
340✔
1281

1282
      uint allocated = uint32(initialPackedCoverTrancheAllocation >> (i * 32));
340✔
1283
      uint deallocateAmount = Math.min(allocated, remainingDeallocationAmount);
340✔
1284

1285
      activeAllocations[currentTrancheIdx] -= deallocateAmount;
340✔
1286
      coverDeallocations[currentTrancheIdx] = deallocateAmount;
340✔
1287
      newPackedCoverAllocations |= (allocated - deallocateAmount) << i * 32;
340✔
1288

1289
      remainingDeallocationAmount -= deallocateAmount;
340✔
1290

1291
      // avoids underflow in the for decrement loop
1292
      if (i == 0) {
340✔
1293
        break;
11✔
1294
      }
1295
    }
1296

1297
    coverTrancheAllocations[params.allocationId] = newPackedCoverAllocations;
57✔
1298

1299
    updateExpiringCoverAmounts(
57✔
1300
      params.productId,
1301
      currentFirstActiveTrancheId,
1302
      Math.divCeil(params.start + params.period, BUCKET_DURATION), // targetBucketId
1303
      coverDeallocations,
1304
      false // isAllocation
1305
    );
1306

1307
    updateStoredAllocations(
57✔
1308
      params.productId,
1309
      currentFirstActiveTrancheId,
1310
      activeAllocations
1311
    );
1312

1313
    emit StakeBurned(amount);
57✔
1314
  }
1315

1316
  /* pool management */
1317

1318
  function setPoolFee(uint newFee) external onlyManager {
1319

1320
    if (newFee > maxPoolFee) {
6✔
1321
      revert PoolFeeExceedsMax();
1✔
1322
    }
1323
    uint oldFee = poolFee;
5✔
1324
    poolFee = uint8(newFee);
5✔
1325

1326
    // passing true because the amount of rewards shares changes
1327
    processExpirations(true);
5✔
1328

1329
    uint fromTrancheId = block.timestamp / TRANCHE_DURATION;
5✔
1330
    uint toTrancheId = fromTrancheId + MAX_ACTIVE_TRANCHES - 1;
5✔
1331
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
5✔
1332

1333
    for (uint trancheId = fromTrancheId; trancheId <= toTrancheId; trancheId++) {
5✔
1334

1335
      // sload
1336
      Deposit memory feeDeposit = deposits[0][trancheId];
40✔
1337

1338
      if (feeDeposit.rewardsShares == 0) {
40✔
1339
        continue;
39✔
1340
      }
1341

1342
      // update pending reward and reward shares
1343
      uint newRewardPerRewardsShare = _accNxmPerRewardsShare.uncheckedSub(feeDeposit.lastAccNxmPerRewardShare);
1✔
1344
      feeDeposit.pendingRewards += (newRewardPerRewardsShare * feeDeposit.rewardsShares / ONE_NXM).toUint96();
1✔
1345
      feeDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
1✔
1346
      // TODO: would using tranche.rewardsShares give a better precision?
1347
      feeDeposit.rewardsShares = (uint(feeDeposit.rewardsShares) * newFee / oldFee).toUint128();
1✔
1348

1349
      // sstore
1350
      deposits[0][trancheId] = feeDeposit;
1✔
1351
    }
1352

1353
    emit PoolFeeChanged(msg.sender, newFee);
5✔
1354
  }
1355

1356
  function setPoolPrivacy(bool _isPrivatePool) external onlyManager {
1357
    isPrivatePool = _isPrivatePool;
6✔
1358
    emit PoolPrivacyChanged(msg.sender, _isPrivatePool);
6✔
1359
  }
1360

1361
  function setPoolDescription(string memory ipfsDescriptionHash) external onlyManager {
1362
    emit PoolDescriptionSet(ipfsDescriptionHash);
×
1363
  }
1364

1365
  /* getters */
1366

1367
  function manager() public override view returns (address) {
1368
    return tokenController.getStakingPoolManager(poolId);
196✔
1369
  }
1370

1371
  function getPoolId() external override view returns (uint) {
1372
    return poolId;
46✔
1373
  }
1374

1375
  function getPoolFee() external override view returns (uint) {
1376
    return poolFee;
4✔
1377
  }
1378

1379
  function getMaxPoolFee() external override view returns (uint) {
1380
    return maxPoolFee;
2✔
1381
  }
1382

1383
  function getActiveStake() external override view returns (uint) {
1384
    return activeStake;
1,147✔
1385
  }
1386

1387
  function getStakeSharesSupply() external override view returns (uint) {
1388
    return stakeSharesSupply;
29✔
1389
  }
1390

1391
  function getRewardsSharesSupply() external override view returns (uint) {
1392
    return rewardsSharesSupply;
12✔
1393
  }
1394

1395
  function getRewardPerSecond() external override view returns (uint) {
1396
    return rewardPerSecond;
12✔
1397
  }
1398

1399
  function getAccNxmPerRewardsShare() external override view returns (uint) {
1400
    return accNxmPerRewardsShare;
35✔
1401
  }
1402

1403
  function getLastAccNxmUpdate() external override view returns (uint) {
1404
    return lastAccNxmUpdate;
15✔
1405
  }
1406

1407
  function getFirstActiveTrancheId() external override view returns (uint) {
1408
    return firstActiveTrancheId;
5✔
1409
  }
1410

1411
  function getFirstActiveBucketId() external override view returns (uint) {
1412
    return firstActiveBucketId;
7✔
1413
  }
1414

1415
  function getNextAllocationId() external override view returns (uint) {
1416
    return lastAllocationId + 1;
15✔
1417
  }
1418

1419
  function getDeposit(uint tokenId, uint trancheId) external override view returns (
1420
    uint lastAccNxmPerRewardShare,
1421
    uint pendingRewards,
1422
    uint stakeShares,
1423
    uint rewardsShares
1424
  ) {
1425
    Deposit memory deposit = deposits[tokenId][trancheId];
27✔
1426
    return (
27✔
1427
      deposit.lastAccNxmPerRewardShare,
1428
      deposit.pendingRewards,
1429
      deposit.stakeShares,
1430
      deposit.rewardsShares
1431
    );
1432
  }
1433

1434
  function getTranche(uint trancheId) external override view returns (
1435
    uint stakeShares,
1436
    uint rewardsShares
1437
  ) {
1438
    Tranche memory tranche = tranches[trancheId];
29✔
1439
    return (
29✔
1440
      tranche.stakeShares,
1441
      tranche.rewardsShares
1442
    );
1443
  }
1444

1445
  function getExpiredTranche(uint trancheId) external override view returns (
1446
    uint accNxmPerRewardShareAtExpiry,
1447
    uint stakeAmountAtExpiry,
1448
    uint stakeSharesSupplyAtExpiry
1449
  ) {
1450
    ExpiredTranche memory expiredTranche = expiredTranches[trancheId];
50✔
1451
    return (
50✔
1452
      expiredTranche.accNxmPerRewardShareAtExpiry,
1453
      expiredTranche.stakeAmountAtExpiry,
1454
      expiredTranche.stakeSharesSupplyAtExpiry
1455
    );
1456
  }
1457

1458
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc