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

NexusMutual / smart-contracts / 9090b462-e8a3-4c34-a12b-54140acaa1ea

pending completion
9090b462-e8a3-4c34-a12b-54140acaa1ea

Pull #678

circleci

Rox
Make token immutable in MemberRoles.sol
Pull Request #678: Chore: Make token immutable in TokenController.sol

769 of 1164 branches covered (66.07%)

Branch coverage included in aggregate %.

28 of 28 new or added lines in 2 files covered. (100.0%)

2224 of 2686 relevant lines covered (82.8%)

90.91 hits per line

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

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

3
pragma solidity ^0.8.16;
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
  address public _unused_minCALockTime;
25
  address public _unused_claimSubmissionGracePeriod;
26

27
  mapping(uint => StakingPoolNXMBalances) public override stakingPoolNXMBalances;
28

29
  // coverId => CoverInfo
30
  mapping(uint => CoverInfo) public override coverInfo;
31

32
  INXMToken public immutable token;
33
  IQuotationData public immutable quotationData;
34
  address public immutable claimsReward;
35
  address public immutable stakingPoolFactory;
36

37
  constructor(
38
    address quotationDataAddress,
39
    address claimsRewardAddress,
40
    address stakingPoolFactoryAddress,
41
    address tokenAddress
42
  ) {
43
    quotationData = IQuotationData(quotationDataAddress);
27✔
44
    claimsReward = claimsRewardAddress;
27✔
45
    stakingPoolFactory = stakingPoolFactoryAddress;
27✔
46
    token = INXMToken(tokenAddress);
27✔
47
  }
48

49
  // TODO: one-time use function, remove after v2 launch
50
  function unlistClaimsReward() external {
51
    token.removeFromWhiteList(claimsReward);
×
52
  }
53

54
  /* ========== DEPENDENCIES ========== */
55

56
  function pooledStaking() internal view returns (IPooledStaking) {
57
    return IPooledStaking(internalContracts[uint(ID.PS)]);
2✔
58
  }
59

60
  function assessment() internal view returns (IAssessment) {
61
    return IAssessment(internalContracts[uint(ID.AS)]);
1✔
62
  }
63

64
  function cover() internal view returns (ICover) {
65
    return ICover(internalContracts[uint(ID.CO)]);
×
66
  }
67

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

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

76
  function changeDependentContractAddress() public override {
77
    internalContracts[uint(ID.PS)] = master.getLatestAddress("PS");
11✔
78
    internalContracts[uint(ID.AS)] = master.getLatestAddress("AS");
11✔
79
    internalContracts[uint(ID.CO)] = master.getLatestAddress("CO");
11✔
80
    internalContracts[uint(ID.GV)] = master.getLatestAddress("GV");
11✔
81
    internalContracts[uint(ID.P1)] = master.getLatestAddress("P1");
11✔
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);
1✔
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);
48✔
105
    if (_to != address(this)) {
48!
106
      token.transfer(_to, _value);
48✔
107
    }
108
    return true;
48✔
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);
4✔
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);
21✔
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);
5✔
135
  }
136

137
  /**
138
  * @dev Mints new token for an address
139
  * @param _member address to reward the minted tokens
140
  * @param _amount number of tokens to mint
141
  */
142
  function mint(address _member, uint _amount) public override onlyInternal {
143
    token.mint(_member, _amount);
8✔
144
  }
145

146
  /**
147
   * @dev Lock the user's tokens
148
   * @param _of user's address.
149
   */
150
  function lockForMemberVote(address _of, uint _days) public override onlyInternal {
151
    token.lockForMemberVote(_of, _days);
14✔
152
  }
153

154
  /**
155
  * @dev Unlocks the withdrawable tokens against CLA of a specified addresses
156
  * @param users  Addresses of users for whom the tokens are unlocked
157
  */
158
  function withdrawClaimAssessmentTokens(address[] calldata users) external whenNotPaused {
159
    for (uint256 i = 0; i < users.length; i++) {
×
160
      if (locked[users[i]]["CLA"].claimed) {
×
161
        continue;
×
162
      }
163
      uint256 amount = locked[users[i]]["CLA"].amount;
×
164
      if (amount > 0) {
×
165
        locked[users[i]]["CLA"].claimed = true;
×
166
        emit Unlocked(users[i], "CLA", amount);
×
167
        token.transfer(users[i], amount);
×
168
      }
169
    }
170
  }
171

172
  /**
173
   * @dev Updates Uint Parameters of a code
174
   * @param code whose details we want to update
175
   * @param value value to set
176
   */
177
  function updateUintParameters(bytes8 code, uint value) external view onlyGovernance {
178
    // silence compiler warnings
179
    code;
×
180
    value;
×
181
    revert("TokenController: invalid param code");
×
182
  }
183

184
  function getLockReasons(address _of) external override view returns (bytes32[] memory reasons) {
185
    return lockReason[_of];
×
186
  }
187

188
  function totalSupply() public override view returns (uint256) {
189
    return token.totalSupply();
1✔
190
  }
191

192
  /// Returns the base voting power not the balance. It is used in governance voting as well as in
193
  /// snapshot voting.
194
  ///
195
  /// @dev Caution, this function is improperly named because reconfiguring snapshot voting was
196
  /// not desired. It accounts for the tokens in the user's wallet as well as tokens locked in
197
  /// assessment and legacy staking deposits. V2 staking deposits are excluded because they are
198
  /// delegated to the pool managers instead.
199
  /// TODO: add stake pool balance for pool operators
200
  ///
201
  /// @param _of  The member address for which the base voting power is calculated.
202
  function totalBalanceOf(address _of) public override view returns (uint256 amount) {
203

204
    amount = token.balanceOf(_of);
1✔
205

206
    // This loop can be removed once all cover notes are withdrawn
207
    for (uint256 i = 0; i < lockReason[_of].length; i++) {
1✔
208
      amount = amount + _tokensLocked(_of, lockReason[_of][i]);
×
209
    }
210

211
    // [todo] Can be removed after PooledStaking is decommissioned
212
    uint stakerReward = pooledStaking().stakerReward(_of);
1✔
213
    uint stakerDeposit = pooledStaking().stakerDeposit(_of);
1✔
214

215
    (
1✔
216
      uint assessmentStake,
217
      /*uint104 rewardsWithdrawableFromIndex*/,
218
      /*uint16 fraudCount*/
219
    ) = assessment().stakeOf(_of);
220

221
    amount += stakerDeposit + stakerReward + assessmentStake;
1✔
222
  }
223

224
  /// Returns the NXM price in ETH. To be use by external protocols.
225
  ///
226
  /// @dev Intended for external protocols - this is a proxy and the contract address won't change
227
  function getTokenPrice() public override view returns (uint tokenPrice) {
228
    return pool().getTokenPrice();
×
229
  }
230

231
  /// Withdraws governance rewards for the given member address
232
  /// @dev This function requires a batchSize that fits in one block. It cannot be 0.
233
  function withdrawGovernanceRewards(
234
    address memberAddress,
235
    uint batchSize
236
  ) public whenNotPaused {
237
    uint governanceRewards = governance().claimReward(memberAddress, batchSize);
5✔
238
    require(governanceRewards > 0, "TokenController: No withdrawable governance rewards");
5✔
239
    token.transfer(memberAddress, governanceRewards);
4✔
240
  }
241

242
  /// Withdraws governance rewards to the destination address. It can only be called by the owner
243
  /// of the rewards.
244
  /// @dev This function requires a batchSize that fits in one block. It cannot be 0.
245
  function withdrawGovernanceRewardsTo(
246
    address destination,
247
    uint batchSize
248
  ) public whenNotPaused {
249
    uint governanceRewards = governance().claimReward(msg.sender, batchSize);
6✔
250
    require(governanceRewards > 0, "TokenController: No withdrawable governance rewards");
6✔
251
    token.transfer(destination, governanceRewards);
5✔
252
  }
253

254
  /// Function used to claim all pending rewards in one tx. It can be used to selectively withdraw
255
  /// rewards.
256
  ///
257
  /// @param forUser           The address for whom the governance and/or assessment rewards are
258
  ///                          withdrawn.
259
  /// @param fromGovernance    When true, governance rewards are withdrawn.
260
  /// @param fromAssessment    When true, assessment rewards are withdrawn.
261
  /// @param batchSize         The maximum number of iterations to avoid unbounded loops when
262
  ///                          withdrawing governance and/or assessment rewards.
263
  /// @param fromStakingPools  An array of structures containing staking pools, token ids and
264
  ///                          tranche ids. See: WithdrawFromStakingPoolParams from ITokenController
265
  ///                          When empty, no staking rewards are withdrawn.
266
  function withdrawPendingRewards(
267
    address forUser,
268
    bool fromGovernance,
269
    bool fromAssessment,
270
    uint batchSize,
271
    WithdrawFromStakingPoolParams[] calldata fromStakingPools
272
  ) external whenNotPaused {
273

274
    if (fromAssessment) {
×
275
      assessment().withdrawRewards(forUser, batchSize.toUint104());
×
276
    }
277

278
    if (fromGovernance) {
×
279
      uint governanceRewards = governance().claimReward(forUser, batchSize);
×
280
      require(governanceRewards > 0, "TokenController: No withdrawable governance rewards");
×
281
      token.transfer(forUser, governanceRewards);
×
282
    }
283

284
    for (uint i = 0; i < fromStakingPools.length; i++) {
×
285

286
      // TODO: external call to user-controlled address (vuln)
287
      IStakingPool stakingPool = IStakingPool(fromStakingPools[i].poolAddress);
×
288

289
      for (uint j = 0; j < fromStakingPools[i].nfts.length; j++) {
×
290
        stakingPool.withdraw(
×
291
          fromStakingPools[i].nfts[j].id,
292
          false, // withdrawStake
293
          true,  // withdrawRewards
294
          fromStakingPools[i].nfts[j].trancheIds
295
        );
296
      }
297
    }
298
  }
299

300
  /**
301
  * @dev Returns tokens locked for a specified address for a
302
  *    specified reason
303
  *
304
  * @param _of The address whose tokens are locked
305
  * @param _reason The reason to query the lock tokens for
306
  */
307
  function _tokensLocked(
308
    address _of,
309
    bytes32 _reason
310
  ) internal view returns (uint256 amount) {
311
    if (!locked[_of][_reason].claimed) {
×
312
      amount = locked[_of][_reason].amount;
×
313
    }
314
  }
315

316
  // Can be removed once all cover notes are withdrawn
317
  function getWithdrawableCoverNotes(
318
    address coverOwner
319
  ) public view returns (
320
    uint[] memory coverIds,
321
    bytes32[] memory lockReasons,
322
    uint withdrawableAmount
323
  ) {
324

325
    uint[] memory allCoverIds = quotationData.getAllCoversOfUser(coverOwner);
×
326
    uint[] memory idsQueue = new uint[](allCoverIds.length);
×
327
    bytes32[] memory lockReasonsQueue = new bytes32[](allCoverIds.length);
×
328
    uint idsQueueLength = 0;
×
329

330
    for (uint i = 0; i < allCoverIds.length; i++) {
×
331
      uint coverId = allCoverIds[i];
×
332
      bytes32 lockReason = keccak256(abi.encodePacked("CN", coverOwner, coverId));
×
333
      uint coverNoteAmount = _tokensLocked(coverOwner, lockReason);
×
334

335
      if (coverNoteAmount > 0) {
×
336
        idsQueue[idsQueueLength] = coverId;
×
337
        lockReasonsQueue[idsQueueLength] = lockReason;
×
338
        withdrawableAmount += coverNoteAmount;
×
339
        idsQueueLength++;
×
340
      }
341
    }
342

343
    coverIds = new uint[](idsQueueLength);
×
344
    lockReasons = new bytes32[](idsQueueLength);
×
345

346
    for (uint i = 0; i < idsQueueLength; i++) {
×
347
      coverIds[i] = idsQueue[i];
×
348
      lockReasons[i] = lockReasonsQueue[i];
×
349
    }
350
  }
351

352
  // Can be removed once all cover notes are withdrawn
353
  function withdrawCoverNote(
354
    address user,
355
    uint[] calldata coverIds,
356
    uint[] calldata indexes
357
  ) external whenNotPaused override {
358

359
    uint reasonCount = lockReason[user].length;
×
360
    require(reasonCount > 0, "TokenController: No locked cover notes found");
×
361
    uint lastReasonIndex = reasonCount - 1;
×
362
    uint totalAmount = 0;
×
363

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

368
    for (uint i = coverIds.length; i > 0; i--) {
×
369

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

375
      uint amount = locked[user][_reason].amount;
×
376
      totalAmount = totalAmount + amount;
×
377
      delete locked[user][_reason];
×
378

379
      if (lastReasonIndex != _reasonIndex) {
×
380
        lockReason[user][_reasonIndex] = lockReason[user][lastReasonIndex];
×
381
      }
382

383
      lockReason[user].pop();
×
384
      emit Unlocked(user, _reason, amount);
×
385

386
      if (lastReasonIndex > 0) {
×
387
        lastReasonIndex = lastReasonIndex - 1;
×
388
      }
389
    }
390

391
    token.transfer(user, totalAmount);
×
392
  }
393

394
  function _stakingPool(uint poolId) internal view returns (address) {
395
    return StakingPoolLibrary.getAddress(stakingPoolFactory, poolId);
111✔
396
  }
397

398
  function mintStakingPoolNXMRewards(uint amount, uint poolId) external {
399
    require(msg.sender == _stakingPool(poolId), "TokenController: msg.sender not staking pool");
39!
400
    token.mint(address(this), amount);
39✔
401
    stakingPoolNXMBalances[poolId].rewards += amount.toUint128();
39✔
402
  }
403

404
  function burnStakingPoolNXMRewards(uint amount, uint poolId) external {
405
    require(msg.sender == _stakingPool(poolId), "TokenController: msg.sender not staking pool");
×
406
    stakingPoolNXMBalances[poolId].rewards -= amount.toUint128();
×
407
    token.burn(amount);
×
408
  }
409

410
  function depositStakedNXM(address from, uint amount, uint poolId) external {
411
    require(msg.sender == _stakingPool(poolId), "TokenController: msg.sender not staking pool");
38!
412
    stakingPoolNXMBalances[poolId].deposits += amount.toUint128();
38✔
413
    token.operatorTransfer(from, amount);
38✔
414
  }
415

416
  function withdrawNXMStakeAndRewards(
417
    address to,
418
    uint stakeToWithdraw,
419
    uint rewardsToWithdraw,
420
    uint poolId
421
  ) external {
422
    require(msg.sender == _stakingPool(poolId), "TokenController: msg.sender not staking pool");
×
423
    StakingPoolNXMBalances memory poolBalances = stakingPoolNXMBalances[poolId];
×
424
    poolBalances.deposits -= stakeToWithdraw.toUint128();
×
425
    poolBalances.rewards -= rewardsToWithdraw.toUint128();
×
426
    stakingPoolNXMBalances[poolId] = poolBalances;
×
427
    token.transfer(to, stakeToWithdraw + rewardsToWithdraw);
×
428
  }
429

430
  function burnStakedNXM(uint amount, uint poolId) external {
431
    require(msg.sender == _stakingPool(poolId), "TokenController: msg.sender not staking pool");
34!
432
    stakingPoolNXMBalances[poolId].deposits -= amount.toUint128();
34✔
433
    token.burn(amount);
34✔
434
  }
435
}
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