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

NexusMutual / smart-contracts / #412

19 Nov 2023 11:20AM UTC coverage: 83.018% (+0.8%) from 82.234%
#412

Pull #926

shark0der
Remove unused Ramm and Cover dependencies from TokenController
Pull Request #926: Feature: Tokenomics

1015 of 1344 branches covered (0.0%)

Branch coverage included in aggregate %.

333 of 334 new or added lines in 9 files covered. (99.7%)

15 existing lines in 4 files now uncovered.

2896 of 3367 relevant lines covered (86.01%)

178.19 hits per line

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

73.76
/contracts/modules/token/TokenController.sol
1
// SPDX-License-Identifier: GPL-3.0-only
2

3
pragma solidity ^0.8.18;
4

5
import "../../interfaces/IAssessment.sol";
6
import "../../interfaces/ICover.sol";
7
import "../../interfaces/IGovernance.sol";
8
import "../../interfaces/INXMToken.sol";
9
import "../../interfaces/IPool.sol";
10
import "../../interfaces/IPooledStaking.sol";
11
import "../../interfaces/IQuotationData.sol";
12
import "../../interfaces/IStakingPool.sol";
13
import "../../interfaces/ITokenController.sol";
14
import "../../libraries/SafeUintCast.sol";
15
import "../../libraries/StakingPoolLibrary.sol";
16
import "../../abstract/MasterAwareV2.sol";
17
import "./external/LockHandler.sol";
18

19
contract TokenController is ITokenController, LockHandler, MasterAwareV2 {
20
  using SafeUintCast for uint;
21

22
  address public _unused_token;
23
  address public _unused_pooledStaking;
24
  uint public _unused_minCALockTime;
25
  uint public _unused_claimSubmissionGracePeriod;
26

27
  // coverId => CoverInfo
28
  mapping(uint => CoverInfo) public override coverInfo;
29

30
  // pool id => { rewards, deposits }
31
  mapping(uint => StakingPoolNXMBalances) public override stakingPoolNXMBalances;
32

33
  // pool id => manager
34
  mapping(uint => address) internal stakingPoolManagers;
35

36
  // pool id => offer
37
  mapping(uint => StakingPoolOwnershipOffer) internal stakingPoolOwnershipOffers;
38

39
  // manager => pool ids
40
  mapping(address => uint[]) internal managerStakingPools;
41

42
  INXMToken public immutable token;
43
  IQuotationData public immutable quotationData;
44
  address public immutable claimsReward;
45
  address public immutable stakingPoolFactory;
46

47
  constructor(
48
    address quotationDataAddress,
49
    address claimsRewardAddress,
50
    address stakingPoolFactoryAddress,
51
    address tokenAddress
52
  ) {
53
    quotationData = IQuotationData(quotationDataAddress);
27✔
54
    claimsReward = claimsRewardAddress;
27✔
55
    stakingPoolFactory = stakingPoolFactoryAddress;
27✔
56
    token = INXMToken(tokenAddress);
27✔
57
  }
58

59
  /* ========== DEPENDENCIES ========== */
60

61
  function pooledStaking() internal view returns (IPooledStaking) {
62
    return IPooledStaking(internalContracts[uint(ID.PS)]);
34✔
63
  }
64

65
  function assessment() internal view returns (IAssessment) {
66
    return IAssessment(internalContracts[uint(ID.AS)]);
25✔
67
  }
68

69
  function governance() internal view returns (IGovernance) {
70
    return IGovernance(internalContracts[uint(ID.GV)]);
20✔
71
  }
72

73
  function pool() internal view returns (IPool) {
74
    return IPool(internalContracts[uint(ID.P1)]);
×
75
  }
76

77
  function changeDependentContractAddress() public override {
78
    internalContracts[uint(ID.PS)] = master.getLatestAddress("PS");
17✔
79
    internalContracts[uint(ID.AS)] = master.getLatestAddress("AS");
17✔
80
    internalContracts[uint(ID.GV)] = master.getLatestAddress("GV");
17✔
81
    internalContracts[uint(ID.P1)] = master.getLatestAddress("P1");
17✔
82
  }
83

84
  /**
85
   * @dev to change the operator address
86
   * @param _newOperator is the new address of operator
87
   */
88
  function changeOperator(address _newOperator) public override onlyGovernance {
89
    token.changeOperator(_newOperator);
2✔
90
  }
91

92
  /**
93
   * @dev Proxies token transfer through this contract to allow staking when members are locked for voting
94
   * @param _from   Source address
95
   * @param _to     Destination address
96
   * @param _value  Amount to transfer
97
   */
98
  function operatorTransfer(
99
    address _from,
100
    address _to,
101
    uint _value
102
  ) external override onlyInternal returns (bool) {
103

104
    token.operatorTransfer(_from, _value);
50✔
105
    if (_to != address(this)) {
50✔
106
      token.transfer(_to, _value);
49✔
107
    }
108
    return true;
50✔
109
  }
110

111
  /**
112
   * @dev burns tokens of an address
113
   * @param _of is the address to burn tokens of
114
   * @param amount is the amount to burn
115
   * @return the boolean status of the burning process
116
   */
117
  function burnFrom(address _of, uint amount) public override onlyInternal returns (bool) {
118
    return token.burnFrom(_of, amount);
6✔
119
  }
120

121
  /**
122
  * @dev Adds an address to whitelist maintained in the contract
123
  * @param _member address to add to whitelist
124
  */
125
  function addToWhitelist(address _member) public virtual override onlyInternal {
126
    token.addToWhiteList(_member);
32✔
127
  }
128

129
  /**
130
  * @dev Removes an address from the whitelist in the token
131
  * @param _member address to remove
132
  */
133
  function removeFromWhitelist(address _member) public override onlyInternal {
134
    token.removeFromWhiteList(_member);
8✔
135
  }
136

137
  /**
138
  * @dev Mints new tokens for an address and checks if the address is a member
139
  * @param _member address to send the minted tokens to
140
  * @param _amount number of tokens to mint
141
  */
142
  function mint(address _member, uint _amount) public override onlyInternal {
143
    _mint(_member, _amount);
13✔
144
  }
145

146
  /**
147
  * @dev Internal function to mint new tokens for an address and checks if the address is a member
148
  * @dev Other internal functions in this contract should use _mint and never token.mint directly
149
  * @param _member address to send the minted tokens to
150
  * @param _amount number of tokens to mint
151
  */
152
  function _mint(address _member, uint _amount) internal {
153
    require(
80✔
154
      _member == address(this) || token.whiteListed(_member),
155
      "TokenController: Address is not a member"
156
    );
157
    token.mint(_member, _amount);
79✔
158
  }
159

160
  /**
161
   * @dev Lock the user's tokens
162
   * @param _of user's address.
163
   */
164
  function lockForMemberVote(address _of, uint _days) public override onlyInternal {
165
    token.lockForMemberVote(_of, _days);
95✔
166
  }
167

168
  /**
169
  * @dev Unlocks the withdrawable tokens against CLA of a specified addresses
170
  * @param users  Addresses of users for whom the tokens are unlocked
171
  */
172
  function withdrawClaimAssessmentTokens(address[] calldata users) external whenNotPaused {
173
    for (uint256 i = 0; i < users.length; i++) {
×
174
      if (locked[users[i]]["CLA"].claimed) {
×
175
        continue;
×
176
      }
177
      uint256 amount = locked[users[i]]["CLA"].amount;
×
178
      if (amount > 0) {
×
179
        locked[users[i]]["CLA"].claimed = true;
×
180
        emit Unlocked(users[i], "CLA", amount);
×
181
        token.transfer(users[i], amount);
×
182
      }
183
    }
184
  }
185

186
  /**
187
   * @dev Updates Uint Parameters of a code
188
   * @param code whose details we want to update
189
   * @param value value to set
190
   */
191
  function updateUintParameters(bytes8 code, uint value) external view onlyGovernance {
192
    // silence compiler warnings
193
    code;
×
194
    value;
×
195
    revert("TokenController: invalid param code");
×
196
  }
197

198
  function getLockReasons(address _of) external override view returns (bytes32[] memory reasons) {
199
    return lockReason[_of];
×
200
  }
201

202
  function totalSupply() public override view returns (uint256) {
203
    return token.totalSupply();
213✔
204
  }
205

206
  /// Returns the base voting power. It is used in governance and snapshot voting.
207
  /// Includes the delegated tokens via staking pools.
208
  ///
209
  /// @param _of  The member address for which the base voting power is calculated.
210
  function totalBalanceOf(address _of) public override view returns (uint) {
211
    return _totalBalanceOf(_of, true);
10✔
212
  }
213

214
  /// Returns the base voting power. It is used in governance and snapshot voting.
215
  /// Does not include the delegated tokens via staking pools in order to act as a fallback if
216
  /// voting including delegations fails for whatever reason.
217
  ///
218
  /// @param _of  The member address for which the base voting power is calculated.
219
  function totalBalanceOfWithoutDelegations(address _of) public override view returns (uint) {
220
    return _totalBalanceOf(_of, false);
7✔
221
  }
222

223
  function _totalBalanceOf(address _of, bool includeManagedStakingPools) internal view returns (uint) {
224

225
    uint amount = token.balanceOf(_of);
17✔
226

227
    // This loop can be removed once all cover notes are withdrawn
228
    for (uint256 i = 0; i < lockReason[_of].length; i++) {
17✔
229
      amount = amount + tokensLocked(_of, lockReason[_of][i]);
×
230
    }
231

232
    // TODO: can be removed after PooledStaking is decommissioned
233
    amount += pooledStaking().stakerReward(_of);
17✔
234
    amount += pooledStaking().stakerDeposit(_of);
17✔
235

236
    (uint assessmentStake,,) = assessment().stakeOf(_of);
17✔
237
    amount += assessmentStake;
17✔
238

239
    if (includeManagedStakingPools) {
17✔
240
      uint managedStakingPoolCount = managerStakingPools[_of].length;
10✔
241
      for (uint i = 0; i < managedStakingPoolCount; i++) {
10✔
242
        uint poolId = managerStakingPools[_of][i];
4✔
243
        amount += stakingPoolNXMBalances[poolId].deposits;
4✔
244
      }
245
    }
246

247
    return amount;
17✔
248
  }
249

250
  /// Returns the NXM price in ETH. To be use by external protocols.
251
  ///
252
  /// @dev Intended for external protocols - this is a proxy and the contract address won't change
253
  function getTokenPrice() public override view returns (uint tokenPrice) {
254
    // get spot price from ramm
UNCOV
255
    return pool().getTokenPrice();
×
256
  }
257

258
  /// Withdraws governance rewards for the given member address
259
  /// @dev This function requires a batchSize that fits in one block. It cannot be 0.
260
  function withdrawGovernanceRewards(
261
    address memberAddress,
262
    uint batchSize
263
  ) public whenNotPaused {
264
    uint governanceRewards = governance().claimReward(memberAddress, batchSize);
5✔
265
    require(governanceRewards > 0, "TokenController: No withdrawable governance rewards");
5✔
266
    token.transfer(memberAddress, governanceRewards);
4✔
267
  }
268

269
  /// Withdraws governance rewards to the destination address. It can only be called by the owner
270
  /// of the rewards.
271
  /// @dev This function requires a batchSize that fits in one block. It cannot be 0.
272
  function withdrawGovernanceRewardsTo(
273
    address destination,
274
    uint batchSize
275
  ) public whenNotPaused {
276
    uint governanceRewards = governance().claimReward(msg.sender, batchSize);
6✔
277
    require(governanceRewards > 0, "TokenController: No withdrawable governance rewards");
6✔
278
    token.transfer(destination, governanceRewards);
5✔
279
  }
280

281
  function getPendingRewards(address member) public view returns (uint) {
282
    (uint totalPendingAmountInNXM,,) = assessment().getRewards(member);
7✔
283
    uint governanceRewards = governance().getPendingReward(member);
7✔
284
    return totalPendingAmountInNXM + governanceRewards;
7✔
285
  }
286

287
  /// Function used to claim all pending rewards in one tx. It can be used to selectively withdraw
288
  /// rewards.
289
  ///
290
  /// @param forUser           The address for whom the governance and/or assessment rewards are
291
  ///                          withdrawn.
292
  /// @param fromGovernance    When true, governance rewards are withdrawn.
293
  /// @param fromAssessment    When true, assessment rewards are withdrawn.
294
  /// @param batchSize         The maximum number of iterations to avoid unbounded loops when
295
  ///                          withdrawing governance and/or assessment rewards.
296
  function withdrawPendingRewards(
297
    address forUser,
298
    bool fromGovernance,
299
    bool fromAssessment,
300
    uint batchSize
301
  ) external whenNotPaused {
302

303
    if (fromAssessment) {
5✔
304
      assessment().withdrawRewards(forUser, batchSize.toUint104());
1✔
305
    }
306

307
    if (fromGovernance) {
5✔
308
      uint governanceRewards = governance().claimReward(forUser, batchSize);
2✔
309
      require(governanceRewards > 0, "TokenController: No withdrawable governance rewards");
2✔
310
      token.transfer(forUser, governanceRewards);
1✔
311
    }
312
  }
313

314
  /**
315
  * @dev Returns tokens locked for a specified address for a
316
  *    specified reason
317
  *
318
  * @param _of The address whose tokens are locked
319
  * @param _reason The reason to query the lock tokens for
320
  */
321
  function tokensLocked(
322
    address _of,
323
    bytes32 _reason
324
  ) public view returns (uint256 amount) {
325
    if (!locked[_of][_reason].claimed) {
7!
326
      amount = locked[_of][_reason].amount;
7✔
327
    }
328
  }
329

330
  // Can be removed once all cover notes are withdrawn
331
  function getWithdrawableCoverNotes(
332
    address coverOwner
333
  ) public view returns (
334
    uint[] memory coverIds,
335
    bytes32[] memory lockReasons,
336
    uint withdrawableAmount
337
  ) {
338

339
    uint[] memory allCoverIds = quotationData.getAllCoversOfUser(coverOwner);
7✔
340
    uint[] memory idsQueue = new uint[](allCoverIds.length);
7✔
341
    bytes32[] memory lockReasonsQueue = new bytes32[](allCoverIds.length);
7✔
342
    uint idsQueueLength = 0;
7✔
343

344
    for (uint i = 0; i < allCoverIds.length; i++) {
7✔
345
      uint coverId = allCoverIds[i];
×
346
      bytes32 lockReason = keccak256(abi.encodePacked("CN", coverOwner, coverId));
×
347
      uint coverNoteAmount = tokensLocked(coverOwner, lockReason);
×
348

349
      if (coverNoteAmount > 0) {
×
350
        idsQueue[idsQueueLength] = coverId;
×
351
        lockReasonsQueue[idsQueueLength] = lockReason;
×
352
        withdrawableAmount += coverNoteAmount;
×
353
        idsQueueLength++;
×
354
      }
355
    }
356
    coverIds = new uint[](idsQueueLength);
7✔
357
    lockReasons = new bytes32[](idsQueueLength);
7✔
358

359
    for (uint i = 0; i < idsQueueLength; i++) {
7✔
360
      coverIds[i] = idsQueue[i];
×
361
      lockReasons[i] = lockReasonsQueue[i];
×
362
    }
363
  }
364

365
  // Can be removed once all cover notes are withdrawn
366
  function withdrawCoverNote(
367
    address user,
368
    uint[] calldata coverIds,
369
    uint[] calldata indexes
370
  ) external whenNotPaused override {
371

372
    uint reasonCount = lockReason[user].length;
×
373
    require(reasonCount > 0, "TokenController: No locked cover notes found");
×
374
    uint lastReasonIndex = reasonCount - 1;
×
375
    uint totalAmount = 0;
×
376

377
    // The iteration is done from the last to first to prevent reason indexes from
378
    // changing due to the way we delete the items (copy last to current and pop last).
379
    // The provided indexes array must be ordered, otherwise reason index checks will fail.
380

381
    for (uint i = coverIds.length; i > 0; i--) {
×
382

383
      // note: cover owner is implicitly checked using the reason hash
384
      bytes32 _reason = keccak256(abi.encodePacked("CN", user, coverIds[i - 1]));
×
385
      uint _reasonIndex = indexes[i - 1];
×
386
      require(lockReason[user][_reasonIndex] == _reason, "TokenController: Bad reason index");
×
387

388
      uint amount = locked[user][_reason].amount;
×
389
      totalAmount = totalAmount + amount;
×
390
      delete locked[user][_reason];
×
391

392
      if (lastReasonIndex != _reasonIndex) {
×
393
        lockReason[user][_reasonIndex] = lockReason[user][lastReasonIndex];
×
394
      }
395

396
      lockReason[user].pop();
×
397
      emit Unlocked(user, _reason, amount);
×
398

399
      if (lastReasonIndex > 0) {
×
400
        lastReasonIndex = lastReasonIndex - 1;
×
401
      }
402
    }
403

404
    token.transfer(user, totalAmount);
×
405
  }
406

407
  function getStakingPoolManager(uint poolId) external override view returns (address) {
408
    return stakingPoolManagers[poolId];
102✔
409
  }
410

411
  function getManagerStakingPools(address manager) external override view returns (uint[] memory) {
412
    return managerStakingPools[manager];
24✔
413
  }
414

415
  function isStakingPoolManager(address member) external override view returns (bool) {
416
    return managerStakingPools[member].length > 0;
25✔
417
  }
418

419
  function getStakingPoolOwnershipOffer(
420
    uint poolId
421
  ) external override view returns (address proposedManager, uint deadline) {
422
    return (
6✔
423
      stakingPoolOwnershipOffers[poolId].proposedManager,
424
      stakingPoolOwnershipOffers[poolId].deadline
425
    );
426
  }
427

428
  /// Transfer ownership of all staking pools managed by a member to a new address. Used when switching membership.
429
  ///
430
  /// @param from  address of the member whose pools are being transferred
431
  /// @param to    the new address of the member
432
  function transferStakingPoolsOwnership(address from, address to) external override onlyInternal {
433

434
    uint stakingPoolCount = managerStakingPools[from].length;
10✔
435

436
    if (stakingPoolCount == 0) {
10✔
437
      return;
5✔
438
    }
439

440
    while (stakingPoolCount > 0) {
5✔
441
      // remove from old
442
      uint poolId = managerStakingPools[from][stakingPoolCount - 1];
24✔
443
      managerStakingPools[from].pop();
24✔
444

445
      // add to new and update manager
446
      managerStakingPools[to].push(poolId);
24✔
447
      stakingPoolManagers[poolId] = to;
24✔
448

449
      stakingPoolCount--;
24✔
450
    }
451
  }
452

453
  function _assignStakingPoolManager(uint poolId, address manager) internal {
454

455
    address previousManager = stakingPoolManagers[poolId];
59✔
456

457
    // remove previous manager
458
    if (previousManager != address(0)) {
59✔
459
      uint managedPoolCount = managerStakingPools[previousManager].length;
6✔
460

461
      // find staking pool id index and remove from previous manager's list
462
      // on-chain iteration is expensive, but we don't expect to have many pools per manager
463
      for (uint i = 0; i < managedPoolCount; i++) {
6✔
464
        if (managerStakingPools[previousManager][i] == poolId) {
7✔
465
          uint lastIndex = managedPoolCount - 1;
6✔
466
          managerStakingPools[previousManager][i] = managerStakingPools[previousManager][lastIndex];
6✔
467
          managerStakingPools[previousManager].pop();
6✔
468
          break;
6✔
469
        }
470
      }
471
    }
472

473
    // add staking pool id to new manager's list
474
    managerStakingPools[manager].push(poolId);
59✔
475
    stakingPoolManagers[poolId] = manager;
59✔
476
  }
477

478
  /// Transfers the ownership of a staking pool to a new address
479
  /// Used by PooledStaking during the migration
480
  ///
481
  /// @param poolId       id of the staking pool
482
  /// @param manager      address of the new manager of the staking pool
483
  function assignStakingPoolManager(uint poolId, address manager) external override onlyInternal {
484
    _assignStakingPoolManager(poolId, manager);
58✔
485
  }
486

487
  /// Creates a ownership transfer offer for a staking pool
488
  /// The offer can be accepted by the proposed manager before the deadline expires
489
  ///
490
  /// @param poolId           id of the staking pool
491
  /// @param proposedManager  address of the proposed manager
492
  /// @param deadline         timestamp after which the offer expires
493
  function createStakingPoolOwnershipOffer(
494
    uint poolId,
495
    address proposedManager,
496
    uint deadline
497
  ) external override {
498
    require(msg.sender == stakingPoolManagers[poolId], "TokenController: Caller is not staking pool manager");
12✔
499
    require(block.timestamp < deadline, "TokenController: Deadline cannot be in the past");
11✔
500
    stakingPoolOwnershipOffers[poolId] = StakingPoolOwnershipOffer(proposedManager, deadline.toUint96());
10✔
501
  }
502

503
  /// Accepts a staking pool ownership offer
504
  ///
505
  /// @param poolId  id of the staking pool
506
  function acceptStakingPoolOwnershipOffer(uint poolId) external override {
507

508
    address oldManager = stakingPoolManagers[poolId];
6✔
509

510
    require(
6✔
511
      block.timestamp > token.isLockedForMV(oldManager),
512
      "TokenController: Current manager is locked for voting in governance"
513
    );
514

515
    require(
5✔
516
      msg.sender == stakingPoolOwnershipOffers[poolId].proposedManager,
517
      "TokenController: Caller is not the proposed manager"
518
    );
519

520
    require(
2✔
521
      stakingPoolOwnershipOffers[poolId].deadline > block.timestamp,
522
      "TokenController: Ownership offer has expired"
523
    );
524

525
    _assignStakingPoolManager(poolId, msg.sender);
1✔
526
    delete stakingPoolOwnershipOffers[poolId];
1✔
527
  }
528

529
  /// Cancels a staking pool ownership offer
530
  ///
531
  /// @param poolId  id of the staking pool
532
  function cancelStakingPoolOwnershipOffer(uint poolId) external override {
533
    require(msg.sender == stakingPoolManagers[poolId], "TokenController: Caller is not staking pool manager");
5✔
534
    delete stakingPoolOwnershipOffers[poolId];
4✔
535
  }
536

537
  function _stakingPool(uint poolId) internal view returns (address) {
538
    return StakingPoolLibrary.getAddress(stakingPoolFactory, poolId);
180✔
539
  }
540

541
  function mintStakingPoolNXMRewards(uint amount, uint poolId) external {
542
    require(msg.sender == _stakingPool(poolId), "TokenController: Caller not a staking pool");
68✔
543
    _mint(address(this), amount);
67✔
544
    stakingPoolNXMBalances[poolId].rewards += amount.toUint128();
67✔
545
  }
546

547
  function burnStakingPoolNXMRewards(uint amount, uint poolId) external {
548
    require(msg.sender == _stakingPool(poolId), "TokenController: Caller not a staking pool");
3✔
549
    stakingPoolNXMBalances[poolId].rewards -= amount.toUint128();
2✔
550
    token.burn(amount);
2✔
551
  }
552

553
  function depositStakedNXM(address from, uint amount, uint poolId) external {
554
    require(msg.sender == _stakingPool(poolId), "TokenController: Caller not a staking pool");
68✔
555
    stakingPoolNXMBalances[poolId].deposits += amount.toUint128();
67✔
556
    token.operatorTransfer(from, amount);
67✔
557
  }
558

559
  function withdrawNXMStakeAndRewards(
560
    address to,
561
    uint stakeToWithdraw,
562
    uint rewardsToWithdraw,
563
    uint poolId
564
  ) external {
565
    require(msg.sender == _stakingPool(poolId), "TokenController: Caller not a staking pool");
4✔
566
    StakingPoolNXMBalances memory poolBalances = stakingPoolNXMBalances[poolId];
3✔
567
    poolBalances.deposits -= stakeToWithdraw.toUint128();
3✔
568
    poolBalances.rewards -= rewardsToWithdraw.toUint128();
3✔
569
    stakingPoolNXMBalances[poolId] = poolBalances;
3✔
570
    token.transfer(to, stakeToWithdraw + rewardsToWithdraw);
3✔
571
  }
572

573
  function burnStakedNXM(uint amount, uint poolId) external {
574
    require(msg.sender == _stakingPool(poolId), "TokenController: Caller not a staking pool");
37✔
575
    stakingPoolNXMBalances[poolId].deposits -= amount.toUint128();
36✔
576
    token.burn(amount);
36✔
577
  }
578
}
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