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

NexusMutual / smart-contracts / 035a8930-1e16-4dfc-afe0-e6c35208e2d5

pending completion
035a8930-1e16-4dfc-afe0-e6c35208e2d5

Pull #642

circleci

Rox
Fix lint, skip script
Pull Request #642: Feature: Migrate PooledStaking

765 of 1160 branches covered (65.95%)

Branch coverage included in aggregate %.

2225 of 2687 relevant lines covered (82.81%)

90.54 hits per line

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

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

3
pragma solidity ^0.8.9;
4

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

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

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

31
  /* storage */
32

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

37
  // supply of pool rewards shares used by tranches
38
  uint128 public rewardsSharesSupply;
39

40
  // slot 2
41
  // accumulated rewarded nxm per reward share
42
  uint96 public accNxmPerRewardsShare;
43

44
  // currently active staked nxm amount
45
  uint96 public activeStake;
46

47
  uint32 public firstActiveTrancheId;
48
  uint32 public firstActiveBucketId;
49

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

57
  uint40 poolId;
58
  uint24 public nextAllocationId;
59

60
  bool public isPrivatePool;
61

62
  uint8 public poolFee;
63
  uint8 public maxPoolFee;
64

65
  // 40 bytes left in slot 3
66

67
  // slot 4
68
  address public manager;
69
  uint32 public totalEffectiveWeight;
70
  uint32 public totalTargetWeight;
71

72
  // 32 bytes left in slot 4
73

74
  // tranche id => tranche data
75
  mapping(uint => Tranche) public tranches;
76

77
  // tranche id => expired tranche data
78
  mapping(uint => ExpiredTranche) public expiredTranches;
79

80
  // reward bucket id => RewardBucket
81
  mapping (uint => uint) public rewardPerSecondCut;
82

83
  // product id => tranche group id => active allocations for a tranche group
84
  mapping(uint => mapping(uint => TrancheAllocationGroup)) public trancheAllocationGroups;
85

86
  // product id => bucket id => bucket tranche group id => tranche group's expiring cover amounts
87
  mapping(uint => mapping(uint => mapping(uint => TrancheGroupBucket))) public expiringCoverBuckets;
88

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

93
  // product id => Product
94
  mapping(uint => StakedProduct) public products;
95

96
  // token id => tranche id => deposit data
97
  mapping(uint => mapping(uint => Deposit)) public deposits;
98

99
  /* immutables */
100

101
  IStakingNFT public immutable stakingNFT;
102
  INXMToken public immutable nxm;
103
  ITokenController public  immutable tokenController;
104
  address public immutable coverContract;
105
  INXMMaster public immutable masterContract;
106

107
  /* constants */
108

109
  // 7 * 13 = 91
110
  uint public constant BUCKET_DURATION = 28 days;
111
  uint public constant TRANCHE_DURATION = 91 days;
112
  uint public constant MAX_ACTIVE_TRANCHES = 8; // 7 whole quarters + 1 partial quarter
113

114
  uint public constant COVER_TRANCHE_GROUP_SIZE = 5;
115
  uint public constant BUCKET_TRANCHE_GROUP_SIZE = 8;
116

117
  uint public constant REWARD_BONUS_PER_TRANCHE_RATIO = 10_00; // 10.00%
118
  uint public constant REWARD_BONUS_PER_TRANCHE_DENOMINATOR = 100_00;
119
  uint public constant MAX_TOTAL_WEIGHT = 20_00; // 20x
120
  uint public constant WEIGHT_DENOMINATOR = 100;
121
  uint public constant REWARDS_DENOMINATOR = 100_00;
122
  uint public constant POOL_FEE_DENOMINATOR = 100;
123

124
  // denominators for cover contract parameters
125
  uint public constant GLOBAL_CAPACITY_DENOMINATOR = 100_00;
126
  uint public constant CAPACITY_REDUCTION_DENOMINATOR = 100_00;
127
  uint public constant INITIAL_PRICE_DENOMINATOR = 100_00;
128
  uint public constant TARGET_PRICE_DENOMINATOR = 100_00;
129

130
  // base price bump
131
  // +0.2% for each 1% of capacity used, ie +20% for 100%
132
  uint public constant PRICE_BUMP_RATIO = 20_00; // 20%
133

134
  // bumped price smoothing
135
  // 0.5% per day
136
  uint public constant PRICE_CHANGE_PER_DAY = 50; // 0.5%
137

138
  // +2% for every 1%, ie +200% for 100%
139
  uint public constant SURGE_PRICE_RATIO = 2 ether;
140

141
  uint public constant SURGE_THRESHOLD_RATIO = 90_00; // 90.00%
142
  uint public constant SURGE_THRESHOLD_DENOMINATOR = 100_00; // 100.00%
143

144
  // 1 nxm = 1e18
145
  uint public constant ONE_NXM = 1 ether;
146

147
  // internally we store capacity using 2 decimals
148
  // 1 nxm of capacity is stored as 100
149
  uint public constant ALLOCATION_UNITS_PER_NXM = 100;
150

151
  // given capacities have 2 decimals
152
  // smallest unit we can allocate is 1e18 / 100 = 1e16 = 0.01 NXM
153
  uint public constant NXM_PER_ALLOCATION_UNIT = ONE_NXM / ALLOCATION_UNITS_PER_NXM;
154

155
  uint public constant MAX_UINT = type(uint).max;
156

157
  modifier onlyCoverContract {
158
    if (msg.sender != coverContract) {
357✔
159
      revert OnlyCoverContract();
2✔
160
    }
161
    _;
355✔
162
  }
163

164
  modifier onlyManager {
165
    if (msg.sender != manager) {
74✔
166
      revert OnlyManager();
4✔
167
    }
168
    _;
70✔
169
  }
170

171
  modifier whenNotPaused {
172
    require(!masterContract.isPause(), "System is paused");
239✔
173
    _;
236✔
174
  }
175

176
  constructor (
177
    address _stakingNFT,
178
    address _token,
179
    address _coverContract,
180
    address _tokenController,
181
    address _master
182
  ) {
183
    stakingNFT = IStakingNFT(_stakingNFT);
9✔
184
    nxm = INXMToken(_token);
9✔
185
    coverContract = _coverContract;
9✔
186
    tokenController = ITokenController(_tokenController);
9✔
187
    masterContract = INXMMaster(_master);
9✔
188
  }
189

190
  function initialize(
191
    address _manager,
192
    bool _isPrivatePool,
193
    uint _initialPoolFee,
194
    uint _maxPoolFee,
195
    ProductInitializationParams[] calldata params,
196
    uint _poolId,
197
    string  calldata ipfsDescriptionHash
198
  ) external onlyCoverContract {
199

200
    if (_initialPoolFee > _maxPoolFee) {
135✔
201
      revert PoolFeeExceedsMax();
1✔
202
    }
203

204
    if (_maxPoolFee >= 100) {
134✔
205
      revert MaxPoolFeeAbove100();
1✔
206
    }
207

208
    manager = _manager;
133✔
209
    isPrivatePool = _isPrivatePool;
133✔
210
    poolFee = uint8(_initialPoolFee);
133✔
211
    maxPoolFee = uint8(_maxPoolFee);
133✔
212
    poolId = _poolId.toUint40();
133✔
213

214
    _setInitialProducts(params);
133✔
215

216
    emit PoolDescriptionSet(ipfsDescriptionHash);
128✔
217
  }
218

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

223
    uint _firstActiveBucketId = firstActiveBucketId;
346✔
224
    uint _firstActiveTrancheId = firstActiveTrancheId;
346✔
225

226
    uint currentBucketId = block.timestamp / BUCKET_DURATION;
346✔
227
    uint currentTrancheId = block.timestamp / TRANCHE_DURATION;
346✔
228

229
    // if the pool is new
230
    if (_firstActiveBucketId == 0) {
346✔
231
      _firstActiveBucketId = currentBucketId;
5✔
232
      _firstActiveTrancheId = currentTrancheId;
5✔
233
    }
234

235
    // if a force update was not requested
236
    if (!updateUntilCurrentTimestamp) {
346✔
237

238
      bool canExpireBuckets = _firstActiveBucketId < currentBucketId;
59✔
239
      bool canExpireTranches = _firstActiveTrancheId < currentTrancheId;
59✔
240

241
      // and if there's nothing to expire
242
      if (!canExpireBuckets && !canExpireTranches) {
59✔
243
        // we can exit
244
        return;
33✔
245
      }
246
    }
247

248
    // SLOAD
249
    uint _activeStake = activeStake;
313✔
250
    uint _rewardPerSecond = rewardPerSecond;
313✔
251
    uint _stakeSharesSupply = stakeSharesSupply;
313✔
252
    uint _rewardsSharesSupply = rewardsSharesSupply;
313✔
253
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
313✔
254
    uint _lastAccNxmUpdate = lastAccNxmUpdate;
313✔
255

256
    // exit early if we already updated in the current block
257
    if (_lastAccNxmUpdate == block.timestamp) {
313✔
258
      return;
6✔
259
    }
260

261
    if (_rewardsSharesSupply == 0) {
307✔
262
      // nothing to do, just update lastAccNxmUpdate
263
      lastAccNxmUpdate = block.timestamp.toUint32();
9✔
264
      return;
9✔
265
    }
266

267
    while (_firstActiveBucketId < currentBucketId || _firstActiveTrancheId < currentTrancheId) {
298✔
268

269
      // what expires first, the bucket or the tranche?
270
      bool bucketExpiresFirst;
538✔
271
      {
538✔
272
        uint nextBucketStart = (_firstActiveBucketId + 1) * BUCKET_DURATION;
538✔
273
        uint nextTrancheStart = (_firstActiveTrancheId + 1) * TRANCHE_DURATION;
538✔
274
        bucketExpiresFirst = nextBucketStart <= nextTrancheStart;
538✔
275
      }
276

277
      if (bucketExpiresFirst) {
538✔
278

279
        // expire a bucket
280
        // each bucket contains a reward reduction - we subtract it when the bucket *starts*!
281

282
        ++_firstActiveBucketId;
415✔
283
        uint bucketStartTime = _firstActiveBucketId * BUCKET_DURATION;
415✔
284
        uint elapsed = bucketStartTime - _lastAccNxmUpdate;
415✔
285

286
        uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
415✔
287
          ? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
288
          : 0;
289

290
        _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
415✔
291

292
        _rewardPerSecond -= rewardPerSecondCut[_firstActiveBucketId];
415✔
293
        _lastAccNxmUpdate = bucketStartTime;
415✔
294

295
        continue;
415✔
296
      }
297

298
      // expire a tranche
299
      // each tranche contains shares - we expire them when the tranche *ends*
300
      // TODO: check if we have to expire the tranche
301
      {
123✔
302
        uint trancheEndTime = (_firstActiveTrancheId + 1) * TRANCHE_DURATION;
123✔
303
        uint elapsed = trancheEndTime - _lastAccNxmUpdate;
123✔
304
        uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
123✔
305
          ? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
306
          : 0;
307
        _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
123✔
308
        _lastAccNxmUpdate = trancheEndTime;
123✔
309

310
        // SSTORE
311
        expiredTranches[_firstActiveTrancheId] = ExpiredTranche(
123✔
312
          _accNxmPerRewardsShare.toUint96(), // accNxmPerRewardShareAtExpiry
313
          _activeStake.toUint96(), // stakeAmountAtExpiry
314
          _stakeSharesSupply.toUint128() // stakeSharesSupplyAtExpiry
315
        );
316

317
        // SLOAD and then SSTORE zero to get the gas refund
318
        Tranche memory expiringTranche = tranches[_firstActiveTrancheId];
123✔
319
        delete tranches[_firstActiveTrancheId];
123✔
320

321
        // the tranche is expired now so we decrease the stake and the shares supply
322
        uint expiredStake = _stakeSharesSupply != 0
123✔
323
          ? (_activeStake * expiringTranche.stakeShares) / _stakeSharesSupply
324
          : 0;
325

326
        _activeStake -= expiredStake;
123✔
327
        _stakeSharesSupply -= expiringTranche.stakeShares;
123✔
328
        _rewardsSharesSupply -= expiringTranche.rewardsShares;
123✔
329

330
        // advance to the next tranche
331
        _firstActiveTrancheId++;
123✔
332
      }
333

334
      // end while
335
    }
336

337
    if (updateUntilCurrentTimestamp) {
298✔
338
      uint elapsed = block.timestamp - _lastAccNxmUpdate;
272✔
339
      uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
272✔
340
        ? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
341
        : 0;
342
      _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
272✔
343
      _lastAccNxmUpdate = block.timestamp;
272✔
344
    }
345

346
    firstActiveTrancheId = _firstActiveTrancheId.toUint32();
298✔
347
    firstActiveBucketId = _firstActiveBucketId.toUint32();
298✔
348

349
    activeStake = _activeStake.toUint96();
298✔
350
    rewardPerSecond = _rewardPerSecond.toUint96();
298✔
351
    accNxmPerRewardsShare = _accNxmPerRewardsShare.toUint96();
298✔
352
    lastAccNxmUpdate = _lastAccNxmUpdate.toUint32();
298✔
353
    stakeSharesSupply = _stakeSharesSupply.toUint128();
298✔
354
    rewardsSharesSupply = _rewardsSharesSupply.toUint128();
298✔
355
  }
356

357
  function depositTo(
358
    uint amount,
359
    uint trancheId,
360
    uint requestTokenId,
361
    address destination
362
  ) public whenNotPaused returns (uint tokenId) {
363

364
    if (isPrivatePool) {
202✔
365
      if (msg.sender != manager) {
5✔
366
        revert PrivatePool();
3✔
367
      }
368
    }
369

370
    if (block.timestamp <= nxm.isLockedForMV(msg.sender)) {
199✔
371
      revert NxmIsLockedForGovernanceVote();
1✔
372
    }
373

374
    {
198✔
375
      uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
198✔
376
      uint maxTranche = _firstActiveTrancheId + MAX_ACTIVE_TRANCHES - 1;
198✔
377

378
      if (amount == 0) {
198✔
379
        revert InsufficientDepositAmount();
1✔
380
      }
381
      if (trancheId > maxTranche) {
197✔
382
        revert RequestedTrancheIsNotYetActive();
1✔
383
      }
384
      if (trancheId < _firstActiveTrancheId) {
196✔
385
        revert RequestedTrancheIsExpired();
3✔
386
      }
387

388
      // if the pool has no previous deposits
389
      if (firstActiveTrancheId == 0) {
193✔
390
        firstActiveTrancheId = _firstActiveTrancheId.toUint32();
116✔
391
        firstActiveBucketId = (block.timestamp / BUCKET_DURATION).toUint32();
116✔
392
        lastAccNxmUpdate = block.timestamp.toUint32();
116✔
393
      } else {
394
        processExpirations(true);
77✔
395
      }
396
    }
397

398
    // storage reads
399
    uint _activeStake = activeStake;
193✔
400
    uint _stakeSharesSupply = stakeSharesSupply;
193✔
401
    uint _rewardsSharesSupply = rewardsSharesSupply;
193✔
402
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
193✔
403
    uint totalAmount;
193✔
404

405
    // deposit to token id = MAX_UINT is not allowed
406
    // we treat it as a flag to create a new token
407
    if (requestTokenId == MAX_UINT) {
193✔
408
      address to = destination == address(0) ? msg.sender : destination;
176✔
409
      tokenId = stakingNFT.mint(poolId, to);
176✔
410
    } else {
411
      // validate token id exists and belongs to this pool
412
      // stakingPoolOf() reverts for non-existent tokens
413
      require(
17✔
414
        stakingNFT.stakingPoolOf(requestTokenId) == poolId,
415
        "StakingPool: Token does not belong to this pool"
416
      );
417
      tokenId = requestTokenId;
15✔
418
    }
419

420
    uint newStakeShares = _stakeSharesSupply == 0
191✔
421
      ? Math.sqrt(amount)
422
      : _stakeSharesSupply * amount / _activeStake;
423

424
    uint newRewardsShares;
191✔
425

426
    // update deposit and pending reward
427
    {
191✔
428
      // conditional read
429
      Deposit memory deposit = requestTokenId == MAX_UINT
191✔
430
        ? Deposit(0, 0, 0, 0)
431
        : deposits[tokenId][trancheId];
432

433
      newRewardsShares = calculateNewRewardShares(
191✔
434
        deposit.stakeShares, // initialStakeShares
435
        newStakeShares,      // newStakeShares
436
        trancheId,   // initialTrancheId
437
        trancheId,   // newTrancheId, the same as initialTrancheId in this case
438
        block.timestamp
439
      );
440

441
      // if we're increasing an existing deposit
442
      if (deposit.rewardsShares != 0) {
191✔
443
        uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(deposit.lastAccNxmPerRewardShare);
3✔
444
        deposit.pendingRewards += (newEarningsPerShare * deposit.rewardsShares / ONE_NXM).toUint96();
3✔
445
      }
446

447
      deposit.stakeShares += newStakeShares.toUint128();
191✔
448
      deposit.rewardsShares += newRewardsShares.toUint96();
191✔
449
      deposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
191✔
450

451
      // store
452
      deposits[tokenId][trancheId] = deposit;
191✔
453
    }
454

455
    // update pool manager's reward shares
456
    {
191✔
457
      Deposit memory feeDeposit = deposits[MAX_UINT][trancheId];
191✔
458

459
      {
191✔
460
        // create fee deposit reward shares
461
        uint newFeeRewardShares = newRewardsShares * poolFee / (POOL_FEE_DENOMINATOR - poolFee);
191✔
462
        newRewardsShares += newFeeRewardShares;
191✔
463

464
        // calculate rewards until now
465
        uint newRewardPerShare = _accNxmPerRewardsShare.uncheckedSub(feeDeposit.lastAccNxmPerRewardShare);
191✔
466
        feeDeposit.pendingRewards += (newRewardPerShare * feeDeposit.rewardsShares / ONE_NXM).toUint96();
191✔
467
        feeDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
191✔
468
        feeDeposit.rewardsShares += newFeeRewardShares.toUint96();
191✔
469
      }
470

471
      deposits[MAX_UINT][trancheId] = feeDeposit;
191✔
472
    }
473

474
    // update tranche
475
    {
191✔
476
      Tranche memory tranche = tranches[trancheId];
191✔
477
      tranche.stakeShares += newStakeShares.toUint128();
191✔
478
      tranche.rewardsShares += newRewardsShares.toUint128();
191✔
479
      tranches[trancheId] = tranche;
191✔
480
    }
481

482
    totalAmount += amount;
191✔
483
    _activeStake += amount;
191✔
484
    _stakeSharesSupply += newStakeShares;
191✔
485
    _rewardsSharesSupply += newRewardsShares;
191✔
486

487
    // transfer nxm from the staker and update the pool deposit balance
488
    tokenController.depositStakedNXM(msg.sender, totalAmount, poolId);
191✔
489

490
    // update globals
491
    activeStake = _activeStake.toUint96();
191✔
492
    stakeSharesSupply = _stakeSharesSupply.toUint128();
191✔
493
    rewardsSharesSupply = _rewardsSharesSupply.toUint128();
191✔
494

495
    emit StakeDeposited(msg.sender, amount, trancheId, tokenId);
191✔
496
  }
497

498
  function getTimeLeftOfTranche(uint trancheId, uint blockTimestamp) internal pure returns (uint) {
499
    uint endDate = (trancheId + 1) * TRANCHE_DURATION;
496✔
500
    return endDate > blockTimestamp ? endDate - blockTimestamp : 0;
496✔
501
  }
502

503
  /// Calculates the amount of new reward shares based on the initial and new stake shares
504
  ///
505
  /// @param initialStakeShares   Amount of stake shares the deposit is already entitled to
506
  /// @param stakeSharesIncrease  Amount of additional stake shares the deposit will be entitled to
507
  /// @param initialTrancheId     The id of the initial tranche that defines the deposit period
508
  /// @param newTrancheId         The new id of the tranche that will define the deposit period
509
  /// @param blockTimestamp       The timestamp of the block when the new shares are recalculated
510
  function calculateNewRewardShares(
511
    uint initialStakeShares,
512
    uint stakeSharesIncrease,
513
    uint initialTrancheId,
514
    uint newTrancheId,
515
    uint blockTimestamp
516
  ) public pure returns (uint) {
517

518
    uint timeLeftOfInitialTranche = getTimeLeftOfTranche(initialTrancheId, blockTimestamp);
248✔
519
    uint timeLeftOfNewTranche = getTimeLeftOfTranche(newTrancheId, blockTimestamp);
248✔
520

521
    // the bonus is based on the the time left and the total amount of stake shares (initial + new)
522
    uint newBonusShares = (initialStakeShares + stakeSharesIncrease)
248✔
523
      * REWARD_BONUS_PER_TRANCHE_RATIO
524
      * timeLeftOfNewTranche
525
      / TRANCHE_DURATION
526
      / REWARD_BONUS_PER_TRANCHE_DENOMINATOR;
527

528
    // for existing deposits, the previous bonus is deducted from the final amount
529
    uint previousBonusSharesDeduction = initialStakeShares
248✔
530
      * REWARD_BONUS_PER_TRANCHE_RATIO
531
      * timeLeftOfInitialTranche
532
      / TRANCHE_DURATION
533
      / REWARD_BONUS_PER_TRANCHE_DENOMINATOR;
534

535
    return stakeSharesIncrease + newBonusShares - previousBonusSharesDeduction;
248✔
536
  }
537

538
  function withdraw(
539
    uint tokenId,
540
    bool withdrawStake,
541
    bool withdrawRewards,
542
    uint[] memory trancheIds
543
  ) public whenNotPaused returns (uint withdrawnStake, uint withdrawnRewards) {
544

545
    uint managerLockedInGovernanceUntil = nxm.isLockedForMV(manager);
16✔
546

547
    // pass false as it does not modify the share supply nor the reward per second
548
    processExpirations(false);
16✔
549

550
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
16✔
551
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
16✔
552
    uint trancheCount = trancheIds.length;
16✔
553

554
    for (uint j = 0; j < trancheCount; j++) {
16✔
555

556
      uint trancheId = trancheIds[j];
22✔
557

558
      Deposit memory deposit = deposits[tokenId][trancheId];
22✔
559

560
      {
22✔
561
        uint trancheRewardsToWithdraw;
22✔
562
        uint trancheStakeToWithdraw;
22✔
563

564
        // can withdraw stake only if the tranche is expired
565
        if (withdrawStake && trancheId < _firstActiveTrancheId) {
22✔
566

567
          // Deposit withdrawals are not permitted while the manager is locked in governance to
568
          // prevent double voting.
569
          if(managerLockedInGovernanceUntil > block.timestamp) {
18✔
570
            revert ManagerNxmIsLockedForGovernanceVote();
1✔
571
          }
572

573
          // calculate the amount of nxm for this deposit
574
          uint stake = expiredTranches[trancheId].stakeAmountAtExpiry;
17✔
575
          uint _stakeSharesSupply = expiredTranches[trancheId].stakeSharesSupplyAtExpiry;
17✔
576
          trancheStakeToWithdraw = stake * deposit.stakeShares / _stakeSharesSupply;
17✔
577
          withdrawnStake += trancheStakeToWithdraw;
17✔
578

579
          // mark as withdrawn
580
          deposit.stakeShares = 0;
17✔
581
        }
582

583
        if (withdrawRewards) {
21✔
584

585
          // if the tranche is expired, use the accumulator value saved at expiration time
586
          uint accNxmPerRewardShareToUse = trancheId < _firstActiveTrancheId
19✔
587
            ? expiredTranches[trancheId].accNxmPerRewardShareAtExpiry
588
            : _accNxmPerRewardsShare;
589

590
          // calculate reward since checkpoint
591
          uint newRewardPerShare = accNxmPerRewardShareToUse.uncheckedSub(deposit.lastAccNxmPerRewardShare);
19✔
592
          trancheRewardsToWithdraw = newRewardPerShare * deposit.rewardsShares / ONE_NXM + deposit.pendingRewards;
19✔
593
          withdrawnRewards += trancheRewardsToWithdraw;
19✔
594

595
          // save checkpoint
596
          deposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
19✔
597
          deposit.pendingRewards = 0;
19✔
598
          deposit.rewardsShares = 0;
19✔
599
        }
600

601
        emit Withdraw(msg.sender, tokenId, trancheId, trancheStakeToWithdraw, trancheRewardsToWithdraw);
21✔
602
      }
603

604
      deposits[tokenId][trancheId] = deposit;
21✔
605
    }
606

607
    address destination = tokenId == MAX_UINT
15✔
608
      ? manager
609
      : stakingNFT.ownerOf(tokenId);
610

611
    tokenController.withdrawNXMStakeAndRewards(
15✔
612
      destination,
613
      withdrawnStake,
614
      withdrawnRewards,
615
      poolId
616
    );
617

618
    return (withdrawnStake, withdrawnRewards);
15✔
619
  }
620

621
  function requestAllocation(
622
    uint amount,
623
    uint previousPremium,
624
    AllocationRequest calldata request
625
  ) external onlyCoverContract returns (uint premium, uint allocationId) {
626

627
    // passing true because we change the reward per second
628
    processExpirations(true);
186✔
629

630
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
186✔
631

632
    uint[] memory trancheAllocations = request.allocationId == type(uint).max
186✔
633
      ? getActiveAllocations(request.productId)
634
      : getActiveAllocationsWithoutCover(
635
          request.productId,
636
          request.allocationId,
637
          request.previousStart,
638
          request.previousExpiration
639
        );
640

641
    // we are only deallocating
642
    // rewards streaming is left as is
643
    if (amount == 0) {
186✔
644

645
      // store deallocated amount
646
      updateStoredAllocations(
1✔
647
        request.productId,
648
        _firstActiveTrancheId,
649
        trancheAllocations
650
      );
651

652
      // no need to charge any premium
653
      // returning maxUint as allocationId since there was no allocation
654
      // and since amount is 0 allocation will not be saved in the segment
655
      return (0, type(uint).max);
1✔
656
    }
657

658
    uint coverAllocationAmount;
185✔
659
    uint initialCapacityUsed;
185✔
660
    uint totalCapacity;
185✔
661
    (
185✔
662
      coverAllocationAmount,
663
      initialCapacityUsed,
664
      totalCapacity,
665
      allocationId
666
    ) = allocate(amount, request, trancheAllocations);
667

668
    // the returned premium value has 18 decimals
669
    premium = getPremium(
176✔
670
      request.productId,
671
      request.period,
672
      coverAllocationAmount,
673
      initialCapacityUsed,
674
      totalCapacity,
675
      request.globalMinPrice,
676
      request.useFixedPrice
677
    );
678

679
    // add new rewards
680
    {
176✔
681
      if(request.rewardRatio > REWARDS_DENOMINATOR) {
176!
682
        revert RewardRatioTooHigh();
×
683
      }
684

685
      uint expirationBucket = Math.divCeil(block.timestamp + request.period, BUCKET_DURATION);
176✔
686
      uint rewardStreamPeriod = expirationBucket * BUCKET_DURATION - block.timestamp;
176✔
687
      uint _rewardPerSecond = (premium * request.rewardRatio / REWARDS_DENOMINATOR) / rewardStreamPeriod;
176✔
688

689
      // store
690
      rewardPerSecondCut[expirationBucket] += _rewardPerSecond;
176✔
691
      rewardPerSecond += _rewardPerSecond.toUint96();
176✔
692

693
      uint rewardsToMint = _rewardPerSecond * rewardStreamPeriod;
176✔
694
      tokenController.mintStakingPoolNXMRewards(rewardsToMint, poolId);
176✔
695
    }
696

697
    // remove previous rewards
698
    if (previousPremium > 0) {
176✔
699

700
      uint prevRewards = previousPremium * request.previousRewardsRatio / REWARDS_DENOMINATOR;
1✔
701
      uint prevExpirationBucket = Math.divCeil(request.previousExpiration, BUCKET_DURATION);
1✔
702
      uint rewardStreamPeriod = prevExpirationBucket * BUCKET_DURATION - request.previousStart;
1✔
703
      uint prevRewardsPerSecond = prevRewards / rewardStreamPeriod;
1✔
704

705
      // store
706
      rewardPerSecondCut[prevExpirationBucket] -= prevRewardsPerSecond;
1✔
707
      rewardPerSecond -= prevRewardsPerSecond.toUint96();
1✔
708

709
      // prevRewardsPerSecond * rewardStreamPeriodLeft
710
      uint rewardsToBurn = prevRewardsPerSecond * (prevExpirationBucket * BUCKET_DURATION - block.timestamp);
1✔
711
      tokenController.burnStakingPoolNXMRewards(rewardsToBurn, poolId);
1✔
712
    }
713

714
    return (premium, allocationId);
176✔
715
  }
716

717
  function getActiveAllocationsWithoutCover(
718
    uint productId,
719
    uint allocationId,
720
    uint start,
721
    uint expiration
722
  ) internal returns (uint[] memory activeAllocations) {
723

724
    uint packedCoverTrancheAllocation = coverTrancheAllocations[allocationId];
8✔
725
    activeAllocations = getActiveAllocations(productId);
8✔
726

727
    uint currentFirstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
8✔
728
    uint[] memory coverAllocations = new uint[](MAX_ACTIVE_TRANCHES);
8✔
729

730
    // number of already expired tranches to skip
731
    // currentFirstActiveTranche - previousFirstActiveTranche
732
    uint offset = currentFirstActiveTrancheId - (start / TRANCHE_DURATION);
8✔
733
    for (uint i = offset; i < MAX_ACTIVE_TRANCHES; i++) {
8✔
734
      uint allocated = uint32(packedCoverTrancheAllocation >> (i * 32));
64✔
735
      uint currentTrancheIdx = i - offset;
64✔
736
      activeAllocations[currentTrancheIdx] -= allocated;
64✔
737
      coverAllocations[currentTrancheIdx] = allocated;
64✔
738
    }
739

740
    // remove expiring cover amounts from buckets
741
    updateExpiringCoverAmounts(
8✔
742
      productId,
743
      currentFirstActiveTrancheId,
744
      Math.divCeil(expiration, BUCKET_DURATION), // targetBucketId
745
      coverAllocations,
746
      false // isAllocation
747
    );
748

749
    return activeAllocations;
8✔
750
  }
751

752
  function getActiveAllocations(
753
    uint productId
754
  ) public view returns (uint[] memory trancheAllocations) {
755

756
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
243✔
757
    uint currentBucket = (block.timestamp / BUCKET_DURATION).toUint16();
243✔
758
    uint lastBucketId;
243✔
759

760
    (trancheAllocations, lastBucketId) = getStoredAllocations(productId, _firstActiveTrancheId);
243✔
761

762
    if (lastBucketId == 0) {
243✔
763
      lastBucketId = currentBucket;
167✔
764
    }
765

766
    for (uint bucketId = lastBucketId + 1; bucketId <= currentBucket; bucketId++) {
243✔
767

768
      uint[] memory expirations = getExpiringCoverAmounts(productId, bucketId, _firstActiveTrancheId);
58✔
769

770
      for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
58✔
771
        trancheAllocations[i] -= expirations[i];
464✔
772
      }
773
    }
774

775
    return trancheAllocations;
243✔
776
  }
777

778
  function getStoredAllocations(
779
    uint productId,
780
    uint firstTrancheId
781
  ) internal view returns (
782
    uint[] memory storedAllocations,
783
    uint16 lastBucketId
784
  ) {
785

786
    storedAllocations = new uint[](MAX_ACTIVE_TRANCHES);
243✔
787

788
    uint firstGroupId = firstTrancheId / COVER_TRANCHE_GROUP_SIZE;
243✔
789
    uint lastGroupId = (firstTrancheId + MAX_ACTIVE_TRANCHES - 1) / COVER_TRANCHE_GROUP_SIZE;
243✔
790

791
    // min 2 and max 3 groups
792
    uint groupCount = lastGroupId - firstGroupId + 1;
243✔
793

794
    TrancheAllocationGroup[] memory allocationGroups = new TrancheAllocationGroup[](groupCount);
243✔
795

796
    for (uint i = 0; i < groupCount; i++) {
243✔
797
      allocationGroups[i] = trancheAllocationGroups[productId][firstGroupId + i];
684✔
798
    }
799

800
    lastBucketId = allocationGroups[0].getLastBucketId();
243✔
801

802
    // flatten groups
803
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
243✔
804
      uint trancheId = firstTrancheId + i;
1,944✔
805
      uint trancheGroupIndex = trancheId / COVER_TRANCHE_GROUP_SIZE - firstGroupId;
1,944✔
806
      uint trancheIndexInGroup = trancheId % COVER_TRANCHE_GROUP_SIZE;
1,944✔
807
      storedAllocations[i] = allocationGroups[trancheGroupIndex].getItemAt(trancheIndexInGroup);
1,944✔
808
    }
809
  }
810

811
  function getExpiringCoverAmounts(
812
    uint productId,
813
    uint bucketId,
814
    uint firstTrancheId
815
  ) internal view returns (uint[] memory expiringCoverAmounts) {
816

817
    expiringCoverAmounts = new uint[](MAX_ACTIVE_TRANCHES);
58✔
818

819
    uint firstGroupId = firstTrancheId / BUCKET_TRANCHE_GROUP_SIZE;
58✔
820
    uint lastGroupId = (firstTrancheId + MAX_ACTIVE_TRANCHES - 1) / BUCKET_TRANCHE_GROUP_SIZE;
58✔
821

822
    // min 1, max 2
823
    uint groupCount = lastGroupId - firstGroupId + 1;
58✔
824
    TrancheGroupBucket[] memory trancheGroupBuckets = new TrancheGroupBucket[](groupCount);
58✔
825

826
    // min 1 and max 2 reads
827
    for (uint i = 0; i < groupCount; i++) {
58✔
828
      trancheGroupBuckets[i] = expiringCoverBuckets[productId][bucketId][firstGroupId + i];
108✔
829
    }
830

831
    // flatten bucket tranche groups
832
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
58✔
833
      uint trancheId = firstTrancheId + i;
464✔
834
      uint trancheGroupIndex = trancheId / BUCKET_TRANCHE_GROUP_SIZE - firstGroupId;
464✔
835
      uint trancheIndexInGroup = trancheId % BUCKET_TRANCHE_GROUP_SIZE;
464✔
836
      expiringCoverAmounts[i] = trancheGroupBuckets[trancheGroupIndex].getItemAt(trancheIndexInGroup);
464✔
837
    }
838

839
    return expiringCoverAmounts;
58✔
840
  }
841

842
  function getActiveTrancheCapacities(
843
    uint productId,
844
    uint globalCapacityRatio,
845
    uint capacityReductionRatio
846
  ) public view returns (
847
    uint[] memory trancheCapacities,
848
    uint totalCapacity
849
  ) {
850

851
    trancheCapacities = getTrancheCapacities(
5✔
852
      productId,
853
      block.timestamp / TRANCHE_DURATION, // first active tranche id
854
      MAX_ACTIVE_TRANCHES,
855
      globalCapacityRatio,
856
      capacityReductionRatio
857
    );
858

859
    totalCapacity = Math.sum(trancheCapacities);
5✔
860

861
    return (trancheCapacities, totalCapacity);
5✔
862
  }
863

864
  function getTrancheCapacities(
865
    uint productId,
866
    uint firstTrancheId,
867
    uint trancheCount,
868
    uint capacityRatio,
869
    uint reductionRatio
870
  ) internal view returns (uint[] memory trancheCapacities) {
871

872
    // TODO: this require statement seems redundant
873
    if(firstTrancheId < block.timestamp / TRANCHE_DURATION) {
323!
874
      revert RequestedTrancheIsExpired();
×
875
    }
876

877
    uint _activeStake = activeStake;
323✔
878
    uint _stakeSharesSupply = stakeSharesSupply;
323✔
879
    trancheCapacities = new uint[](trancheCount);
323✔
880

881
    if (_stakeSharesSupply == 0) {
323✔
882
      return trancheCapacities;
60✔
883
    }
884

885
    uint multiplier =
263✔
886
      capacityRatio
887
      * (CAPACITY_REDUCTION_DENOMINATOR - reductionRatio)
888
      * products[productId].targetWeight;
889

890
    uint denominator =
263✔
891
      GLOBAL_CAPACITY_DENOMINATOR
892
      * CAPACITY_REDUCTION_DENOMINATOR
893
      * WEIGHT_DENOMINATOR;
894

895
    for (uint i = 0; i < trancheCount; i++) {
263✔
896
      uint trancheStake = (_activeStake * tranches[firstTrancheId + i].stakeShares / _stakeSharesSupply);
2,046✔
897
      trancheCapacities[i] = trancheStake * multiplier / denominator / NXM_PER_ALLOCATION_UNIT;
2,046✔
898
    }
899

900
    return trancheCapacities;
263✔
901
  }
902

903
  function allocate(
904
    uint amount,
905
    AllocationRequest calldata request,
906
    uint[] memory trancheAllocations
907
  ) internal returns (
908
    uint coverAllocationAmount,
909
    uint initialCapacityUsed,
910
    uint totalCapacity,
911
    uint allocationId
912
  ) {
913

914
    if (request.allocationId == type(uint).max) {
185✔
915
      allocationId = nextAllocationId;
178✔
916
      nextAllocationId++;
178✔
917
    } else {
918
      allocationId = request.allocationId;
7✔
919
    }
920

921
    coverAllocationAmount = Math.divCeil(amount, NXM_PER_ALLOCATION_UNIT);
185✔
922

923
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
185✔
924
    uint firstTrancheIdToUse = (block.timestamp + request.period + request.gracePeriod) / TRANCHE_DURATION;
185✔
925
    uint startIndex = firstTrancheIdToUse - _firstActiveTrancheId;
185✔
926

927
    uint[] memory coverAllocations = new uint[](MAX_ACTIVE_TRANCHES);
185✔
928
    uint[] memory trancheCapacities = getTrancheCapacities(
185✔
929
      request.productId,
930
      firstTrancheIdToUse,
931
      MAX_ACTIVE_TRANCHES - startIndex, // count
932
      request.globalCapacityRatio,
933
      request.capacityReductionRatio
934
    );
935

936
    {
185✔
937
      uint remainingAmount = coverAllocationAmount;
185✔
938
      uint packedCoverAllocations;
185✔
939

940
      for (uint i = startIndex; i < MAX_ACTIVE_TRANCHES; i++) {
185✔
941

942
        initialCapacityUsed += trancheAllocations[i];
1,422✔
943
        totalCapacity += trancheCapacities[i - startIndex];
1,422✔
944

945
        if (remainingAmount == 0) {
1,422✔
946
          // not breaking out of the for loop because we need the total capacity calculated above
947
          continue;
1,008✔
948
        }
949

950
        if (trancheAllocations[i] >= trancheCapacities[i - startIndex]) {
414✔
951
          // no capacity left in this tranche
952
          continue;
210✔
953
        }
954

955
        uint allocatedAmount = Math.min(trancheCapacities[i - startIndex] - trancheAllocations[i], remainingAmount);
204✔
956

957
        coverAllocations[i] = allocatedAmount;
204✔
958
        trancheAllocations[i] += allocatedAmount;
204✔
959
        remainingAmount -= allocatedAmount;
204✔
960

961
        packedCoverAllocations |= allocatedAmount << i * 32;
204✔
962
      }
963

964
      coverTrancheAllocations[allocationId] = packedCoverAllocations;
185✔
965

966
      if (remainingAmount != 0) {
185✔
967
        revert InsufficientCapacity();
8✔
968
      }
969
    }
970

971
    updateExpiringCoverAmounts(
177✔
972
      request.productId,
973
      _firstActiveTrancheId,
974
      Math.divCeil(block.timestamp + request.period, BUCKET_DURATION), // targetBucketId
975
      coverAllocations,
976
      true // isAllocation
977
    );
978

979
    updateStoredAllocations(
176✔
980
      request.productId,
981
      _firstActiveTrancheId,
982
      trancheAllocations
983
    );
984

985
    return (coverAllocationAmount, initialCapacityUsed, totalCapacity, allocationId);
176✔
986
  }
987

988
  function updateStoredAllocations(
989
    uint productId,
990
    uint firstTrancheId,
991
    uint[] memory allocations
992
  ) internal {
993

994
    uint firstGroupId = firstTrancheId / COVER_TRANCHE_GROUP_SIZE;
177✔
995
    uint lastGroupId = (firstTrancheId + MAX_ACTIVE_TRANCHES - 1) / COVER_TRANCHE_GROUP_SIZE;
177✔
996
    uint groupCount = lastGroupId - firstGroupId + 1;
177✔
997

998
    TrancheAllocationGroup[] memory allocationGroups = new TrancheAllocationGroup[](groupCount);
177✔
999

1000
    // min 2 and max 3 reads
1001
    for (uint i = 0; i < groupCount; i++) {
177✔
1002
      allocationGroups[i] = trancheAllocationGroups[productId][firstGroupId + i];
506✔
1003
    }
1004

1005
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
177✔
1006

1007
      uint trancheId = firstTrancheId + i;
1,416✔
1008
      uint trancheGroupIndex = trancheId / COVER_TRANCHE_GROUP_SIZE - firstGroupId;
1,416✔
1009
      uint trancheIndexInGroup = trancheId % COVER_TRANCHE_GROUP_SIZE;
1,416✔
1010

1011
      // setItemAt does not mutate so we have to reassign it
1012
      allocationGroups[trancheGroupIndex] = allocationGroups[trancheGroupIndex].setItemAt(
1,416✔
1013
        trancheIndexInGroup,
1014
        allocations[i].toUint48()
1015
      );
1016
    }
1017

1018
    uint16 currentBucket = (block.timestamp / BUCKET_DURATION).toUint16();
177✔
1019

1020
    for (uint i = 0; i < groupCount; i++) {
177✔
1021
      trancheAllocationGroups[productId][firstGroupId + i] = allocationGroups[i].setLastBucketId(currentBucket);
506✔
1022
    }
1023
  }
1024

1025
  function updateExpiringCoverAmounts(
1026
    uint productId,
1027
    uint firstTrancheId,
1028
    uint targetBucketId,
1029
    uint[] memory coverTrancheAllocation,
1030
    bool isAllocation
1031
  ) internal {
1032

1033
    uint firstGroupId = firstTrancheId / BUCKET_TRANCHE_GROUP_SIZE;
185✔
1034
    uint lastGroupId = (firstTrancheId + MAX_ACTIVE_TRANCHES - 1) / BUCKET_TRANCHE_GROUP_SIZE;
185✔
1035
    uint groupCount = lastGroupId - firstGroupId + 1;
185✔
1036

1037
    TrancheGroupBucket[] memory trancheGroupBuckets = new TrancheGroupBucket[](groupCount);
185✔
1038

1039
    // min 1 and max 2 reads
1040
    for (uint i = 0; i < groupCount; i++) {
185✔
1041
      trancheGroupBuckets[i] = expiringCoverBuckets[productId][targetBucketId][firstGroupId + i];
368✔
1042
    }
1043

1044
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
185✔
1045

1046
      uint trancheId = firstTrancheId + i;
1,476✔
1047
      uint trancheGroupId = trancheId / BUCKET_TRANCHE_GROUP_SIZE - firstGroupId;
1,476✔
1048
      uint trancheIndexInGroup = trancheId % BUCKET_TRANCHE_GROUP_SIZE;
1,476✔
1049

1050
      uint32 expiringAmount = trancheGroupBuckets[trancheGroupId].getItemAt(trancheIndexInGroup);
1,476✔
1051
      uint32 trancheAllocation = coverTrancheAllocation[i].toUint32();
1,476✔
1052

1053
      if (isAllocation) {
1,475✔
1054
        expiringAmount += trancheAllocation;
1,411✔
1055
      } else {
1056
        expiringAmount -= trancheAllocation;
64✔
1057
      }
1058

1059
      // setItemAt does not mutate so we have to reassign it
1060
      trancheGroupBuckets[trancheGroupId] = trancheGroupBuckets[trancheGroupId].setItemAt(
1,475✔
1061
        trancheIndexInGroup,
1062
        expiringAmount
1063
      );
1064
    }
1065

1066
    for (uint i = 0; i < groupCount; i++) {
184✔
1067
      expiringCoverBuckets[productId][targetBucketId][firstGroupId + i] = trancheGroupBuckets[i];
366✔
1068
    }
1069
  }
1070

1071
  /// Extends the period of an existing deposit until a tranche that ends further into the future
1072
  ///
1073
  /// @param tokenId           The id of the NFT that proves the ownership of the deposit.
1074
  /// @param initialTrancheId  The id of the tranche the deposit is already a part of.
1075
  /// @param newTrancheId      The id of the new tranche determining the new deposit period.
1076
  /// @param topUpAmount       An optional amount if the user wants to also increase the deposit
1077
  function extendDeposit(
1078
    uint tokenId,
1079
    uint initialTrancheId,
1080
    uint newTrancheId,
1081
    uint topUpAmount
1082
  ) external whenNotPaused {
1083

1084
    // token id MAX_UINT is only used for pool manager fee tracking, no deposits allowed
1085
    if(tokenId == MAX_UINT) {
18✔
1086
      revert InvalidTokenId();
1✔
1087
    }
1088
    if (!stakingNFT.isApprovedOrOwner(msg.sender, tokenId)) {
17✔
1089
      revert NotTokenOwnerOrApproved();
1✔
1090
    }
1091

1092
    uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION;
16✔
1093

1094
    {
16✔
1095
      if (initialTrancheId >= newTrancheId) {
16✔
1096
        revert NewTrancheEndsBeforeInitialTranche();
1✔
1097
      }
1098

1099
      uint maxTrancheId = _firstActiveTrancheId + MAX_ACTIVE_TRANCHES - 1;
15✔
1100

1101
      if (newTrancheId > maxTrancheId) {
15✔
1102
        revert RequestedTrancheIsNotYetActive();
1✔
1103
      }
1104
      if (newTrancheId < firstActiveTrancheId) {
14!
1105
        revert RequestedTrancheIsExpired();
×
1106
      }
1107
    }
1108

1109
    // if the initial tranche is expired, withdraw everything and make a new deposit
1110
    // this requires the user to have grante sufficient allowance
1111
    if (initialTrancheId < _firstActiveTrancheId) {
14✔
1112

1113
      uint[] memory trancheIds = new uint[](1);
6✔
1114
      trancheIds[0] = initialTrancheId;
6✔
1115

1116
      (uint withdrawnStake, /* uint rewardsToWithdraw */) = withdraw(
6✔
1117
        tokenId,
1118
        true, // withdraw the deposit
1119
        true, // withdraw the rewards
1120
        trancheIds
1121
      );
1122

1123
      depositTo(withdrawnStake + topUpAmount, newTrancheId, tokenId, msg.sender);
6✔
1124

1125
      return;
4✔
1126
      // done! skip the rest of the function.
1127
    }
1128

1129
    if (isPrivatePool) {
8✔
1130
      if (msg.sender != manager) {
1!
1131
        revert PrivatePool();
1✔
1132
      }
1133
    }
1134

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

1137
    // passing true because we mint reward shares
1138
    processExpirations(true);
7✔
1139

1140
    Deposit memory initialDeposit = deposits[tokenId][initialTrancheId];
7✔
1141
    Deposit memory updatedDeposit = deposits[tokenId][newTrancheId];
7✔
1142

1143
    uint _activeStake = activeStake;
7✔
1144
    uint _stakeSharesSupply = stakeSharesSupply;
7✔
1145
    uint newStakeShares;
7✔
1146

1147
    // calculate the new stake shares if there's a deposit top up
1148
    if (topUpAmount > 0) {
7✔
1149
      newStakeShares = _stakeSharesSupply * topUpAmount / _activeStake;
5✔
1150
      activeStake = (_activeStake + topUpAmount).toUint96();
5✔
1151
    }
1152

1153
    // calculate the new reward shares
1154
    uint newRewardsShares = calculateNewRewardShares(
7✔
1155
      initialDeposit.stakeShares,
1156
      newStakeShares,
1157
      initialTrancheId,
1158
      newTrancheId,
1159
      block.timestamp
1160
    );
1161

1162
    {
7✔
1163
      Tranche memory initialTranche = tranches[initialTrancheId];
7✔
1164
      Tranche memory newTranche = tranches[newTrancheId];
7✔
1165

1166
      // move the shares to the new tranche
1167
      initialTranche.stakeShares -= initialDeposit.stakeShares;
7✔
1168
      initialTranche.rewardsShares -= initialDeposit.rewardsShares;
7✔
1169
      newTranche.stakeShares += initialDeposit.stakeShares + newStakeShares.toUint128();
7✔
1170
      newTranche.rewardsShares += (initialDeposit.rewardsShares + newRewardsShares).toUint128();
7✔
1171

1172
      // store the updated tranches
1173
      tranches[initialTrancheId] = initialTranche;
7✔
1174
      tranches[newTrancheId] = newTranche;
7✔
1175
    }
1176

1177
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
7✔
1178

1179
    // if there already is a deposit on the new tranche, calculate its pending rewards
1180
    if (updatedDeposit.lastAccNxmPerRewardShare != 0) {
7!
1181
      uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(updatedDeposit.lastAccNxmPerRewardShare);
×
1182
      updatedDeposit.pendingRewards += (newEarningsPerShare * updatedDeposit.rewardsShares / ONE_NXM).toUint96();
×
1183
    }
1184

1185
    // calculate the rewards for the deposit being extended and move them to the new deposit
1186
    {
7✔
1187
      uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(initialDeposit.lastAccNxmPerRewardShare);
7✔
1188
      updatedDeposit.pendingRewards += (newEarningsPerShare * initialDeposit.rewardsShares / ONE_NXM).toUint96();
7✔
1189
      updatedDeposit.pendingRewards += initialDeposit.pendingRewards;
7✔
1190
    }
1191

1192
    updatedDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
7✔
1193
    updatedDeposit.stakeShares += (initialDeposit.stakeShares + newStakeShares).toUint128();
7✔
1194
    updatedDeposit.rewardsShares += (initialDeposit.rewardsShares + newRewardsShares).toUint128();
7✔
1195

1196
    // everything is moved, delete the initial deposit
1197
    delete deposits[tokenId][initialTrancheId];
7✔
1198

1199
    // store the new deposit.
1200
    deposits[tokenId][newTrancheId] = updatedDeposit;
7✔
1201

1202
    // update global shares supply
1203
    stakeSharesSupply = (_stakeSharesSupply + newStakeShares).toUint128();
7✔
1204
    rewardsSharesSupply += newRewardsShares.toUint128();
7✔
1205

1206
    // transfer nxm from the staker and update the pool deposit balance
1207
    tokenController.depositStakedNXM(msg.sender, topUpAmount, poolId);
7✔
1208

1209
    emit DepositExtended(msg.sender, tokenId, initialTrancheId, newTrancheId, topUpAmount);
7✔
1210
  }
1211

1212
  function burnStake(uint amount) external onlyCoverContract {
1213

1214
    // TODO: block the pool if we perform 100% of the stake
1215

1216
    // passing false because neither the amount of shares nor the reward per second are changed
1217
    processExpirations(false);
34✔
1218

1219
    // sload
1220
    uint initialStake = activeStake;
34✔
1221

1222
    // leaving 1 wei to avoid division by zero
1223
    uint burnAmount = amount >= initialStake ? initialStake - 1 : amount;
34✔
1224
    tokenController.burnStakedNXM(burnAmount, poolId);
34✔
1225

1226
    // sstore
1227
    activeStake = (initialStake - burnAmount).toUint96();
34✔
1228

1229
    emit StakeBurned(burnAmount);
34✔
1230
  }
1231

1232
  /* views */
1233

1234
  function getActiveStake() external view returns (uint) {
1235
    block.timestamp; // prevents warning about function being pure
×
1236
    return 0;
×
1237
  }
1238

1239
  function getProductStake(
1240
    uint productId, uint coverExpirationDate
1241
  ) public view returns (uint) {
1242
    productId;
×
1243
    coverExpirationDate;
×
1244
    block.timestamp;
×
1245
    return 0;
×
1246
  }
1247

1248
  function getAllocatedProductStake(uint productId) public view returns (uint) {
1249
    productId;
×
1250
    block.timestamp;
×
1251
    return 0;
×
1252
  }
1253

1254
  function getFreeProductStake(
1255
    uint productId, uint coverExpirationDate
1256
  ) external view returns (uint) {
1257
    productId;
×
1258
    coverExpirationDate;
×
1259
    block.timestamp;
×
1260
    return 0;
×
1261
  }
1262

1263
  /* pool management */
1264

1265
  function recalculateEffectiveWeights(uint[] calldata productIds) external {
1266
    (
1✔
1267
    uint globalCapacityRatio,
1268
    /* globalMinPriceRatio */,
1269
    /* initialPriceRatios */,
1270
    /* capacityReductionRatios */
1271
    uint[] memory capacityReductionRatios
1272
    ) = ICover(coverContract).getPriceAndCapacityRatios(productIds);
1273

1274
    uint _totalEffectiveWeight = totalEffectiveWeight;
1✔
1275

1276
    for (uint i = 0; i < productIds.length; i++) {
1✔
1277
      uint productId = productIds[i];
20✔
1278
      StakedProduct memory _product = products[productId];
20✔
1279

1280
      uint16 previousEffectiveWeight = _product.lastEffectiveWeight;
20✔
1281
      _product.lastEffectiveWeight = getEffectiveWeight(
20✔
1282
        productId,
1283
        _product.targetWeight,
1284
        globalCapacityRatio,
1285
        capacityReductionRatios[i]
1286
      );
1287
      _totalEffectiveWeight = _totalEffectiveWeight - previousEffectiveWeight + _product.lastEffectiveWeight;
20✔
1288
      products[productId] = _product;
20✔
1289
    }
1290
    totalEffectiveWeight = _totalEffectiveWeight.toUint32();
1✔
1291
  }
1292

1293
  function setProducts(StakedProductParam[] memory params) external onlyManager {
1294
    uint numProducts = params.length;
57✔
1295
    uint[] memory productIds = new uint[](numProducts);
57✔
1296

1297
    for (uint i = 0; i < numProducts; i++) {
57✔
1298
      productIds[i] = params[i].productId;
120✔
1299
      if (!ICover(coverContract).isPoolAllowed(params[i].productId, poolId)) {
120!
1300
         revert PoolNotAllowedForThisProduct();
×
1301
      }
1302
    }
1303
    (
57✔
1304
      uint globalCapacityRatio,
1305
      uint globalMinPriceRatio,
1306
      uint[] memory initialPriceRatios,
1307
      uint[] memory capacityReductionRatios
1308
    ) = ICover(coverContract).getPriceAndCapacityRatios(productIds);
1309

1310
    uint _totalTargetWeight = totalTargetWeight;
56✔
1311
    uint _totalEffectiveWeight = totalEffectiveWeight;
56✔
1312
    bool targetWeightIncreased;
56✔
1313

1314
    for (uint i = 0; i < numProducts; i++) {
56✔
1315
      StakedProductParam memory _param = params[i];
119✔
1316
      StakedProduct memory _product = products[_param.productId];
119✔
1317

1318
      // if this is a new product
1319
      if (_product.bumpedPriceUpdateTime == 0) {
119✔
1320
        // initialize the bumpedPrice
1321
        _product.bumpedPrice = initialPriceRatios[i].toUint96();
96✔
1322
        _product.bumpedPriceUpdateTime = uint32(block.timestamp);
96✔
1323
        // and make sure we set the price and the target weight
1324
        if (!_param.setTargetPrice) {
96✔
1325
          revert MustSetPriceForNewProducts();
1✔
1326
        }
1327
        if (!_param.setTargetWeight) {
95!
1328
          revert MustSetWeightForNewProducts();
×
1329
        }
1330
      }
1331

1332
      if (_param.setTargetPrice) {
118✔
1333
        if (_param.targetPrice > TARGET_PRICE_DENOMINATOR) {
117✔
1334
          revert TargetPriceTooHigh();
1✔
1335
        }
1336
        if (_param.targetPrice < globalMinPriceRatio) {
116✔
1337
          revert TargetPriceBelowMin();
1✔
1338
        }
1339
        _product.targetPrice = _param.targetPrice;
115✔
1340
      }
1341

1342
      // if setTargetWeight is set - effective weight must be recalculated
1343
      if (_param.setTargetWeight && !_param.recalculateEffectiveWeight) {
116✔
1344
        revert MustRecalculateEffectiveWeight();
2✔
1345
      }
1346

1347
      // Must recalculate effectiveWeight to adjust targetWeight
1348
      if (_param.recalculateEffectiveWeight) {
114!
1349

1350
        if (_param.setTargetWeight) {
114✔
1351
          if (_param.targetWeight > WEIGHT_DENOMINATOR) {
112✔
1352
            revert TargetWeightTooHigh();
1✔
1353
          }
1354

1355
          // totalEffectiveWeight cannot be above the max unless target  weight is not increased
1356
          if (!targetWeightIncreased) {
111✔
1357
            targetWeightIncreased = _param.targetWeight > _product.targetWeight;
52✔
1358
          }
1359
          _totalTargetWeight = _totalTargetWeight - _product.targetWeight + _param.targetWeight;
111✔
1360
          _product.targetWeight = _param.targetWeight;
111✔
1361
        }
1362

1363
        // subtract the previous effective weight
1364
        _totalEffectiveWeight -= _product.lastEffectiveWeight;
113✔
1365

1366
        _product.lastEffectiveWeight = getEffectiveWeight(
113✔
1367
          _param.productId,
1368
          _product.targetWeight,
1369
          globalCapacityRatio,
1370
          capacityReductionRatios[i]
1371
        );
1372

1373
        // add the new effective weight
1374
        _totalEffectiveWeight += _product.lastEffectiveWeight;
113✔
1375
      }
1376

1377
      // sstore
1378
      products[_param.productId] = _product;
113✔
1379

1380
      emit ProductUpdated(_param.productId, _param.targetWeight, _param.targetPrice);
113✔
1381
    }
1382

1383
    if (_totalTargetWeight > MAX_TOTAL_WEIGHT) {
50✔
1384
      revert TotalTargetWeightExceeded();
3✔
1385
    }
1386

1387
    if (targetWeightIncreased) {
47✔
1388
      if (_totalEffectiveWeight > MAX_TOTAL_WEIGHT) {
28!
1389
        revert TotalEffectiveWeightExceeded();
×
1390
      }
1391
    }
1392

1393
    totalTargetWeight = _totalTargetWeight.toUint32();
47✔
1394
    totalEffectiveWeight = _totalEffectiveWeight.toUint32();
47✔
1395
  }
1396

1397
  function getEffectiveWeight(
1398
    uint productId,
1399
    uint targetWeight,
1400
    uint globalCapacityRatio,
1401
    uint capacityReductionRatio
1402
  ) internal view returns (uint16 effectiveWeight) {
1403

1404
    uint[] memory trancheCapacities = getTrancheCapacities(
133✔
1405
      productId,
1406
      block.timestamp / TRANCHE_DURATION, // first active tranche id
1407
      MAX_ACTIVE_TRANCHES,
1408
      globalCapacityRatio,
1409
      capacityReductionRatio
1410
    );
1411

1412
    uint totalCapacity = Math.sum(trancheCapacities);
133✔
1413

1414
    if (totalCapacity == 0) {
133✔
1415
      return targetWeight.toUint16();
97✔
1416
    }
1417

1418
    uint[] memory activeAllocations = getActiveAllocations(productId);
36✔
1419
    uint totalAllocation = Math.sum(activeAllocations);
36✔
1420
    uint actualWeight = Math.min(totalAllocation * WEIGHT_DENOMINATOR / totalCapacity, type(uint16).max);
36✔
1421

1422
    return Math.max(targetWeight, actualWeight).toUint16();
36✔
1423
  }
1424

1425
  function _setInitialProducts(ProductInitializationParams[] memory params) internal {
1426
    uint32 _totalTargetWeight = totalTargetWeight;
133✔
1427

1428
    for (uint i = 0; i < params.length; i++) {
133✔
1429
      ProductInitializationParams memory param = params[i];
282✔
1430
      StakedProduct storage _product = products[param.productId];
282✔
1431
      if (param.targetPrice > TARGET_PRICE_DENOMINATOR) {
282✔
1432
        revert TargetPriceTooHigh();
1✔
1433
      }
1434
      if (param.weight > WEIGHT_DENOMINATOR) {
281✔
1435
        revert TargetWeightTooHigh();
2✔
1436
      }
1437
      _product.bumpedPrice = param.initialPrice;
279✔
1438
      _product.bumpedPriceUpdateTime = uint32(block.timestamp);
279✔
1439
      _product.targetPrice = param.targetPrice;
279✔
1440
      _product.targetWeight = param.weight;
279✔
1441
      _totalTargetWeight += param.weight;
279✔
1442
    }
1443

1444
    if (_totalTargetWeight > MAX_TOTAL_WEIGHT) {
130✔
1445
      revert TotalTargetWeightExceeded();
2✔
1446
    }
1447
    totalTargetWeight = _totalTargetWeight;
128✔
1448
    totalEffectiveWeight = totalTargetWeight;
128✔
1449
  }
1450

1451
  function setPoolFee(uint newFee) external onlyManager {
1452

1453
    if (newFee > maxPoolFee) {
6✔
1454
      revert PoolFeeExceedsMax();
1✔
1455
    }
1456
    uint oldFee = poolFee;
5✔
1457
    poolFee = uint8(newFee);
5✔
1458

1459
    // passing true because the amount of rewards shares changes
1460
    processExpirations(true);
5✔
1461

1462
    uint fromTrancheId = block.timestamp / TRANCHE_DURATION;
5✔
1463
    uint toTrancheId = fromTrancheId + MAX_ACTIVE_TRANCHES - 1;
5✔
1464
    uint _accNxmPerRewardsShare = accNxmPerRewardsShare;
5✔
1465

1466
    for (uint trancheId = fromTrancheId; trancheId <= toTrancheId; trancheId++) {
5✔
1467

1468
      // sload
1469
      Deposit memory feeDeposit = deposits[MAX_UINT][trancheId];
40✔
1470

1471
      if (feeDeposit.rewardsShares == 0) {
40✔
1472
        continue;
39✔
1473
      }
1474

1475
      // update pending reward and reward shares
1476
      uint newRewardPerRewardsShare = _accNxmPerRewardsShare.uncheckedSub(feeDeposit.lastAccNxmPerRewardShare);
1✔
1477
      feeDeposit.pendingRewards += (newRewardPerRewardsShare * feeDeposit.rewardsShares / ONE_NXM).toUint96();
1✔
1478
      feeDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare.toUint96();
1✔
1479
      // TODO: would using tranche.rewardsShares give a better precision?
1480
      feeDeposit.rewardsShares = (uint(feeDeposit.rewardsShares) * newFee / oldFee).toUint96();
1✔
1481

1482
      // sstore
1483
      deposits[MAX_UINT][trancheId] = feeDeposit;
1✔
1484
    }
1485

1486
    emit PoolFeeChanged(msg.sender, newFee);
5✔
1487
  }
1488

1489
  function setPoolPrivacy(bool _isPrivatePool) external onlyManager {
1490
    isPrivatePool = _isPrivatePool;
6✔
1491

1492
    emit PoolPrivacyChanged(msg.sender, _isPrivatePool);
6✔
1493
  }
1494

1495
  function setPoolDescription(string memory ipfsDescriptionHash) external onlyManager {
1496
    emit PoolDescriptionSet(ipfsDescriptionHash);
1✔
1497
  }
1498

1499
  /* pricing code */
1500

1501
  function getPremium(
1502
    uint productId,
1503
    uint period,
1504
    uint coverAmount,
1505
    uint initialCapacityUsed,
1506
    uint totalCapacity,
1507
    uint globalMinPrice,
1508
    bool useFixedPrice
1509
  ) internal returns (uint premium) {
1510

1511
    StakedProduct memory product = products[productId];
176✔
1512
    uint targetPrice = Math.max(product.targetPrice, globalMinPrice);
176✔
1513

1514
    if (useFixedPrice) {
176!
1515
      return calculateFixedPricePremium(period, coverAmount, targetPrice);
×
1516
    }
1517

1518
    (premium, product) = calculatePremium(
176✔
1519
      product,
1520
      period,
1521
      coverAmount,
1522
      initialCapacityUsed,
1523
      totalCapacity,
1524
      targetPrice,
1525
      block.timestamp
1526
    );
1527

1528
    // sstore
1529
    products[productId] = product;
176✔
1530

1531
    return premium;
176✔
1532
  }
1533

1534
  function calculateFixedPricePremium(
1535
    uint coverAmount,
1536
    uint period,
1537
    uint fixedPrice
1538
  ) public pure returns (uint) {
1539

1540
    uint premiumPerYear =
1✔
1541
      coverAmount
1542
      * NXM_PER_ALLOCATION_UNIT
1543
      * fixedPrice
1544
      / TARGET_PRICE_DENOMINATOR;
1545

1546
    return premiumPerYear * period / 365 days;
1✔
1547
  }
1548

1549
  function calculatePremium(
1550
    StakedProduct memory product,
1551
    uint period,
1552
    uint coverAmount,
1553
    uint initialCapacityUsed,
1554
    uint totalCapacity,
1555
    uint targetPrice,
1556
    uint currentBlockTimestamp
1557
  ) public pure returns (uint premium, StakedProduct memory) {
1558

1559
    uint basePrice;
192✔
1560
    {
192✔
1561
      // use previously recorded bumped price and apply time based smoothing towards target price
1562
      uint timeSinceLastUpdate = currentBlockTimestamp - product.bumpedPriceUpdateTime;
192✔
1563
      uint priceDrop = PRICE_CHANGE_PER_DAY * timeSinceLastUpdate / 1 days;
192✔
1564

1565
      // basePrice = max(targetPrice, bumpedPrice - priceDrop)
1566
      // rewritten to avoid underflow
1567
      basePrice = product.bumpedPrice < targetPrice + priceDrop
192✔
1568
        ? targetPrice
1569
        : product.bumpedPrice - priceDrop;
1570
    }
1571

1572
    // calculate the bumped price by applying the price bump
1573
    uint priceBump = PRICE_BUMP_RATIO * coverAmount / totalCapacity;
192✔
1574
    product.bumpedPrice = (basePrice + priceBump).toUint96();
191✔
1575
    product.bumpedPriceUpdateTime = uint32(currentBlockTimestamp);
191✔
1576

1577
    // use calculated base price and apply surge pricing if applicable
1578
    uint premiumPerYear = calculatePremiumPerYear(
191✔
1579
      basePrice,
1580
      coverAmount,
1581
      initialCapacityUsed,
1582
      totalCapacity
1583
    );
1584

1585
    // calculate the premium for the requested period
1586
    return (premiumPerYear * period / 365 days, product);
191✔
1587
  }
1588

1589
  function calculatePremiumPerYear(
1590
    uint basePrice,
1591
    uint coverAmount,
1592
    uint initialCapacityUsed,
1593
    uint totalCapacity
1594
  ) public pure returns (uint) {
1595
    // cover amount has 2 decimals (100 = 1 unit)
1596
    // scale coverAmount to 18 decimals and apply price percentage
1597
    uint basePremium = coverAmount * NXM_PER_ALLOCATION_UNIT * basePrice / TARGET_PRICE_DENOMINATOR;
192✔
1598
    uint finalCapacityUsed = initialCapacityUsed + coverAmount;
192✔
1599

1600
    // surge price is applied for the capacity used above SURGE_THRESHOLD_RATIO.
1601
    // the surge price starts at zero and increases linearly.
1602
    // to simplify things, we're working with fractions/ratios instead of percentages,
1603
    // ie 0 to 1 instead of 0% to 100%, 100% = 1 (a unit).
1604
    //
1605
    // surgeThreshold = SURGE_THRESHOLD_RATIO / SURGE_THRESHOLD_DENOMINATOR
1606
    //                = 90_00 / 100_00 = 0.9
1607
    uint surgeStartPoint = totalCapacity * SURGE_THRESHOLD_RATIO / SURGE_THRESHOLD_DENOMINATOR;
192✔
1608

1609
    // Capacity and surge pricing
1610
    //
1611
    //        i        f                         s
1612
    //   ▓▓▓▓▓░░░░░░░░░                          ▒▒▒▒▒▒▒▒▒▒
1613
    //
1614
    //  i - initial capacity used
1615
    //  f - final capacity used
1616
    //  s - surge start point
1617

1618
    // if surge does not apply just return base premium
1619
    // i < f <= s case
1620
    if (finalCapacityUsed <= surgeStartPoint) {
192✔
1621
      return basePremium;
136✔
1622
    }
1623

1624
    // calculate the premium amount incurred due to surge pricing
1625
    uint amountOnSurge = finalCapacityUsed - surgeStartPoint;
56✔
1626
    uint surgePremium = calculateSurgePremium(amountOnSurge, totalCapacity);
56✔
1627

1628
    // if the capacity start point is before the surge start point
1629
    // the surge premium starts at zero, so we just return it
1630
    // i <= s < f case
1631
    if (initialCapacityUsed <= surgeStartPoint) {
55✔
1632
      return basePremium + surgePremium;
53✔
1633
    }
1634

1635
    // otherwise we need to subtract the part that was already used by other covers
1636
    // s < i < f case
1637
    uint amountOnSurgeSkipped = initialCapacityUsed - surgeStartPoint;
2✔
1638
    uint surgePremiumSkipped = calculateSurgePremium(amountOnSurgeSkipped, totalCapacity);
2✔
1639

1640
    return basePremium + surgePremium - surgePremiumSkipped;
2✔
1641
  }
1642

1643
  // Calculates the premium for a given cover amount starting with the surge point
1644
  function calculateSurgePremium(
1645
    uint amountOnSurge,
1646
    uint totalCapacity
1647
  ) public pure returns (uint) {
1648

1649
    // for every percent of capacity used, the surge price has a +2% increase per annum
1650
    // meaning a +200% increase for 100%, ie x2 for a whole unit (100%) of capacity in ratio terms
1651
    //
1652
    // coverToCapacityRatio = amountOnSurge / totalCapacity
1653
    // surgePriceStart = 0
1654
    // surgePriceEnd = SURGE_PRICE_RATIO * coverToCapacityRatio
1655
    //
1656
    // surgePremium = amountOnSurge * surgePriceEnd / 2
1657
    //              = amountOnSurge * SURGE_PRICE_RATIO * coverToCapacityRatio / 2
1658
    //              = amountOnSurge * SURGE_PRICE_RATIO * amountOnSurge / totalCapacity / 2
1659

1660
    uint surgePremium = amountOnSurge * SURGE_PRICE_RATIO * amountOnSurge / totalCapacity / 2;
59✔
1661

1662
    // amountOnSurge has two decimals
1663
    // dividing by ALLOCATION_UNITS_PER_NXM (=100) to normalize the result
1664
    return surgePremium / ALLOCATION_UNITS_PER_NXM;
58✔
1665
  }
1666

1667
}
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

© 2026 Coveralls, Inc