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

NexusMutual / smart-contracts / #1329

19 Aug 2025 03:57PM UTC coverage: 53.568% (+0.04%) from 53.527%
#1329

Pull #1428

shark0der
test: fix governance test after cap introduction
Pull Request #1428: feat: v3 solidity todos

824 of 1662 branches covered (49.58%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 3 files covered. (100.0%)

21 existing lines in 3 files now uncovered.

1608 of 2878 relevant lines covered (55.87%)

13.36 hits per line

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

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

3
pragma solidity ^0.8.28;
4

5
import "../../abstract/RegistryAware.sol";
6
import "../../interfaces/IAssessment.sol";
7
import "../../interfaces/INXMToken.sol";
8
import "../../interfaces/IPool.sol";
9
import "../../interfaces/IStakingNFT.sol";
10
import "../../interfaces/IStakingPool.sol";
11
import "../../interfaces/ITokenController.sol";
12
import "../../interfaces/ITokenControllerErrors.sol";
13
import "../../libraries/SafeUintCast.sol";
14
import "../../libraries/StakingPoolLibrary.sol";
15

16
contract TokenController is ITokenController, ITokenControllerErrors, RegistryAware {
17
  using SafeUintCast for uint;
18

19
  // master + mapping + lockReason + locked
20
  uint[4] internal _unused;
21

22
  address internal _unused_token;
23
  address internal _unused_pooledStaking;
24
  uint internal _unused_minCALockTime;
25
  uint internal _unused_claimSubmissionGracePeriod;
26
  uint internal _unused_coverInfo; // was mapping(uint coverId => uint CoverInfo)
27

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

31
  // pool id => manager
32
  mapping(uint => address) internal stakingPoolManagers;
33

34
  // pool id => offer
35
  mapping(uint => StakingPoolOwnershipOffer) internal stakingPoolOwnershipOffers;
36

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

40
  INXMToken public immutable token;
41
  address public immutable stakingPoolFactory;
42
  IStakingNFT public immutable stakingNFT;
43
  IPool public immutable pool;
44

45
  modifier onlyStakingPool(uint poolId) {
46
    require(msg.sender == _stakingPool(poolId), OnlyStakingPool());
32✔
47
    _;
27✔
48
  }
49

50
  constructor (address _registry) RegistryAware(_registry) {
51
    stakingPoolFactory = fetch(C_STAKING_POOL_FACTORY);
2✔
52
    token = INXMToken(fetch(C_TOKEN));
2✔
53
    stakingNFT = IStakingNFT(fetch(C_STAKING_NFT));
2✔
54
    pool = IPool(fetch(C_POOL));
2✔
55
  }
56

57
  /* ========== DEPENDENCIES ========== */
58

59
  function stakingPool(uint poolId) internal view returns (IStakingPool) {
60
    return IStakingPool(_stakingPool(poolId));
1✔
61
  }
62

63
  function _stakingPool(uint poolId) internal view returns (address) {
64
    return StakingPoolLibrary.getAddress(stakingPoolFactory, poolId);
33✔
65
  }
66

67
  /// @dev Changes the operator address.
68
  /// @param _newOperator The new address of the operator.
69
  function changeOperator(address _newOperator) public override onlyContracts(C_GOVERNOR) {
2✔
70
    token.changeOperator(_newOperator);
1✔
71
  }
72

73
  /// @dev Proxies token transfer through this contract to allow staking when members are locked for voting.
74
  /// @param _from  The source address.
75
  /// @param _to    The destination address.
76
  /// @param _value The amount to transfer.
77
  function operatorTransfer(
78
    address _from,
79
    address _to,
80
    uint _value
81
  ) external override onlyContracts(C_COVER) returns (bool) {
3✔
82

83
    token.operatorTransfer(_from, _value);
2✔
84
    if (_to != address(this)) {
2✔
85
      token.transfer(_to, _value);
1✔
86
    }
87
    return true;
2✔
88
  }
89

90
  /// @dev Burns tokens of an address.
91
  /// @param _of     The address to burn tokens of.
92
  /// @param amount  The amount to burn.
93
  /// @return        The boolean status of the burning process.
94
  function burnFrom(address _of, uint amount) public override onlyContracts(C_COVER + C_RAMM) returns (bool) {
2✔
95
    return token.burnFrom(_of, amount);
1✔
96
  }
97

98
  /// @dev Adds an address to the whitelist maintained in the contract.
99
  /// @param _member The address to add to the whitelist.
100
  function addToWhitelist(address _member) public virtual override onlyContracts(C_REGISTRY) {
3✔
101
    token.addToWhiteList(_member);
2✔
102
  }
103

104
  /// @notice Removes an address from the whitelist in the token
105
  /// @dev Requires the member's token balance to be zero before removal.
106
  /// @param _member The address to remove.
107
  function removeFromWhitelist(address _member) public override onlyContracts(C_REGISTRY) {
3✔
108
    require(token.balanceOf(_member) == 0, MemberBalanceNotZero());
2✔
109
    token.removeFromWhiteList(_member);
1✔
110
  }
111

112
  /// @notice Switches membership from one address to another, transferring all tokens.
113
  /// @dev Transfers the full token balance from the old address to the new one, updates whitelist status accordingly.
114
  /// @param from The address to transfer membership from.
115
  /// @param to The address to transfer membership to.
116
  function switchMembership(
117
    address from,
118
    address to,
119
    bool includeNxmTokens
120
  ) external override onlyContracts(C_REGISTRY) {
3✔
121

122
    token.addToWhiteList(to);
2✔
123
    token.removeFromWhiteList(from);
2✔
124

125
    if (includeNxmTokens) {
2✔
126
      token.transferFrom(from, to, token.balanceOf(from));
1✔
127
    }
128

129
    uint stakingPoolCount = managerStakingPools[from].length;
2✔
130

131
    while (stakingPoolCount > 0) {
2✔
132
      // remove from old
133
      uint poolId = managerStakingPools[from][stakingPoolCount - 1];
2✔
134
      managerStakingPools[from].pop();
2✔
135

136
      // add to new and update manager
137
      managerStakingPools[to].push(poolId);
2✔
138
      stakingPoolManagers[poolId] = to;
2✔
139

140
      stakingPoolCount--;
2✔
141
    }
142
  }
143

144
  /// @dev Mints new tokens for an address and checks if the address is a member.
145
  /// @param _member The address to send the minted tokens to.
146
  /// @param _amount The number of tokens to mint.
147
  function mint(address _member, uint _amount) public override onlyContracts(C_RAMM) {
7✔
148
    _mint(_member, _amount);
6✔
149
  }
150

151
  /// @dev Internal function to mint new tokens for an address and checks if the address is a member.
152
  /// @dev Other internal functions in this contract should use _mint and never token.mint directly.
153
  /// @param _member The address to send the minted tokens to.
154
  /// @param _amount The number of tokens to mint.
155
  function _mint(address _member, uint _amount) internal {
156

157
    require(
13✔
158
      _member == address(this) || token.whiteListed(_member),
12✔
159
      CantMintToNonMemberAddress()
160
    );
161
    token.mint(_member, _amount);
12✔
162
  }
163

164
  /// @dev Locks the user's tokens.
165
  /// @param _of    The user's address.
166
  /// @param _days  The number of days to lock the tokens.
167
  function lockForMemberVote(address _of, uint _days) public override onlyContracts(C_GOVERNOR) {
2✔
168
    token.lockForMemberVote(_of, _days);
1✔
169
  }
170

171
  /// @notice Returns the total supply of the NXM token.
172
  /// @return The total supply of the NXM token.
173
  function totalSupply() public override view returns (uint) {
174
    return token.totalSupply();
2✔
175
  }
176

177
  /// @notice Returns the base voting power. It is used in governance and snapshot voting.
178
  ///         Includes the delegated tokens via staking pools.
179
  /// @param _of  The member address for which the base voting power is calculated.
180
  function totalBalanceOf(address _of) public override view returns (uint) {
181
    return _totalBalanceOf(_of, true);
6✔
182
  }
183

184
  /// @notice Returns the base voting power. It is used in governance and snapshot voting.
185
  /// @dev    Does not include the delegated tokens via staking pools in order to act as a fallback if
186
  ///         voting including delegations fails for whatever reason.
187
  /// @param _of  The member address for which the base voting power is calculated.
188
  function totalBalanceOfWithoutDelegations(address _of) public override view returns (uint) {
189
    return _totalBalanceOf(_of, false);
4✔
190
  }
191

192
  function _totalBalanceOf(address _of, bool includeManagedStakingPools) internal view returns (uint) {
193

194
    uint amount = token.balanceOf(_of);
10✔
195

196
    if (includeManagedStakingPools) {
10✔
197
      uint managedStakingPoolCount = managerStakingPools[_of].length;
6✔
198
      for (uint i = 0; i < managedStakingPoolCount; i++) {
6✔
199
        uint poolId = managerStakingPools[_of][i];
4✔
200
        amount += stakingPoolNXMBalances[poolId].deposits;
4✔
201
      }
202
    }
203

204
    return amount;
10✔
205
  }
206

207
  /// @notice Returns the NXM price in ETH. To be use by external protocols.
208
  /// @dev Intended for external protocols - this is a proxy and the contract address won't change
209
  function getTokenPrice() public override view returns (uint tokenPrice) {
210
    // get spot price from ramm
UNCOV
211
    return pool.getTokenPrice();
×
212
  }
213

214
  /// @notice Withdraws NXM from the Nexus platform based on specified options.
215
  /// @dev    Ensure the NXM is available and not locked before withdrawal. Only set flags in `WithdrawNxmOptions` for
216
  ///         withdrawable NXM. Reverts if some of the NXM being withdrawn is locked or unavailable.
217
  /// @param stakingPoolDeposits        Details for withdrawing staking pools stake and rewards. Empty array to skip
218
  /// @param stakingPoolManagerRewards  Details for withdrawing staking pools manager rewards. Empty array to skip
219
  ///                                   specific assesment stake or rewards withdrawal.
220
  function withdrawNXM(
221
    StakingPoolDeposit[] calldata stakingPoolDeposits,
222
    StakingPoolManagerReward[] calldata stakingPoolManagerRewards
223
  ) external whenNotPaused(PAUSE_GLOBAL) {
3!
224
    // staking pool rewards and stake
225
    for (uint i = 0; i < stakingPoolDeposits.length; i++) {
3✔
226
      uint tokenId = stakingPoolDeposits[i].tokenId;
2✔
227
      uint poolId = stakingNFT.stakingPoolOf(tokenId);
2✔
228
      stakingPool(poolId).withdraw(tokenId, true, true, stakingPoolDeposits[i].trancheIds);
1✔
229
    }
230

231
    // staking pool manager rewards
232
    for (uint i = 0; i < stakingPoolManagerRewards.length; i++) {
2✔
UNCOV
233
      uint poolId = stakingPoolManagerRewards[i].poolId;
×
UNCOV
234
      stakingPool(poolId).withdraw(0, false, true, stakingPoolManagerRewards[i].trancheIds);
×
235
    }
236
  }
237

238
  /// @notice Retrieves the manager of a specific staking pool.
239
  /// @param poolId  The ID of the staking pool.
240
  /// @return        The address of the staking pool manager.
241
  function getStakingPoolManager(uint poolId) external override view returns (address) {
242
    return stakingPoolManagers[poolId];
23✔
243
  }
244

245
  /// @notice Retrieves the staking pools managed by a specific manager.
246
  /// @param manager  The address of the manager.
247
  /// @return         An array of staking pool IDs managed by the specified manager.
248
  function getManagerStakingPools(address manager) external override view returns (uint[] memory) {
249
    return managerStakingPools[manager];
8✔
250
  }
251

252
  /// @notice Checks if a given address is a staking pool manager.
253
  /// @param member  The address to check.
254
  function isStakingPoolManager(address member) external override view returns (bool) {
255
    return managerStakingPools[member].length > 0;
9✔
256
  }
257

258
  /// @notice Retrieves the ownership offer details for a specific staking pool.
259
  /// @param poolId            The ID of the staking pool.
260
  /// @return proposedManager  The address of the proposed new manager.
261
  /// @return deadline         The deadline for accepting the ownership offer.
262
  function getStakingPoolOwnershipOffer(
263
    uint poolId
264
  ) external override view returns (address proposedManager, uint deadline) {
265
    return (
6✔
266
      stakingPoolOwnershipOffers[poolId].proposedManager,
267
      stakingPoolOwnershipOffers[poolId].deadline
268
    );
269
  }
270

271
  function _assignStakingPoolManager(uint poolId, address manager) internal {
272

273
    address previousManager = stakingPoolManagers[poolId];
32✔
274

275
    // remove previous manager
276
    if (previousManager != address(0)) {
32✔
277
      uint managedPoolCount = managerStakingPools[previousManager].length;
6✔
278

279
      // find staking pool id index and remove from previous manager's list
280
      // on-chain iteration is expensive, but we don't expect to have many pools per manager
281
      for (uint i = 0; i < managedPoolCount; i++) {
6✔
282
        if (managerStakingPools[previousManager][i] == poolId) {
7✔
283
          uint lastIndex = managedPoolCount - 1;
6✔
284
          managerStakingPools[previousManager][i] = managerStakingPools[previousManager][lastIndex];
6✔
285
          managerStakingPools[previousManager].pop();
6✔
286
          break;
6✔
287
        }
288
      }
289
    }
290

291
    // add staking pool id to new manager's list
292
    managerStakingPools[manager].push(poolId);
32✔
293
    stakingPoolManagers[poolId] = manager;
32✔
294
  }
295

296
  /// @notice Transfers the ownership of a staking pool to a new address
297
  /// @dev    Used by PooledStaking during the migration
298
  /// @param poolId       id of the staking pool
299
  /// @param manager      address of the new manager of the staking pool
300
  function assignStakingPoolManager(uint poolId, address manager) external override onlyContracts(C_STAKING_PRODUCTS) {
32✔
301
    _assignStakingPoolManager(poolId, manager);
31✔
302
  }
303

304
  /// @notice Creates a ownership transfer offer for a staking pool
305
  /// @dev    The offer can be accepted by the proposed manager before the deadline expires
306
  /// @param poolId           id of the staking pool
307
  /// @param proposedManager  address of the proposed manager
308
  /// @param deadline         timestamp after which the offer expires
309
  function createStakingPoolOwnershipOffer(
310
    uint poolId,
311
    address proposedManager,
312
    uint deadline
313
  ) external override {
314

315
    require(msg.sender == stakingPoolManagers[poolId], OnlyStakingPoolManager());
12✔
316
    require(block.timestamp < deadline, DeadlinePassed());
11✔
317

318
    stakingPoolOwnershipOffers[poolId] = StakingPoolOwnershipOffer(proposedManager, deadline.toUint96());
10✔
319
  }
320

321
  /// @notice Accepts a staking pool ownership offer
322
  /// @param poolId  id of the staking pool
323
  function acceptStakingPoolOwnershipOffer(uint poolId) external override {
324

325
    address oldManager = stakingPoolManagers[poolId];
6✔
326

327
    require(block.timestamp > token.isLockedForMV(oldManager), ManagerIsLockedForVoting());
6✔
328
    require(msg.sender == stakingPoolOwnershipOffers[poolId].proposedManager, OnlyProposedManager());
5✔
329
    require(stakingPoolOwnershipOffers[poolId].deadline > block.timestamp, OwnershipOfferHasExpired());
2✔
330

331
    _assignStakingPoolManager(poolId, msg.sender);
1✔
332

333
    delete stakingPoolOwnershipOffers[poolId];
1✔
334
  }
335

336
  /// @notice Cancels a staking pool ownership offer
337
  /// @param poolId  id of the staking pool
338
  function cancelStakingPoolOwnershipOffer(uint poolId) external override {
339

340
    require(msg.sender == stakingPoolManagers[poolId], OnlyStakingPoolManager());
5✔
341

342
    delete stakingPoolOwnershipOffers[poolId];
4✔
343
  }
344

345
  /// @notice Mints a specified amount of NXM rewards for a staking pool.
346
  /// @dev    Only callable by the staking pool associated with the given poolId
347
  /// @param amount  The amount of NXM to mint.
348
  /// @param poolId  The ID of the staking pool.
349
  function mintStakingPoolNXMRewards(uint amount, uint poolId) external override onlyStakingPool(poolId) {
8✔
350
    _mint(address(this), amount);
7✔
351
    stakingPoolNXMBalances[poolId].rewards += amount.toUint128();
7✔
352
  }
353

354
  /// @notice Burns a specified amount of NXM rewards from a staking pool.
355
  /// @dev    Only callable by the staking pool associated with the given poolId
356
  /// @param amount  The amount of NXM to burn.
357
  /// @param poolId  The ID of the staking pool.
358
  function burnStakingPoolNXMRewards(uint amount, uint poolId) external override onlyStakingPool(poolId) {
3✔
359
    stakingPoolNXMBalances[poolId].rewards -= amount.toUint128();
2✔
360
    token.burn(amount);
2✔
361
  }
362

363
  /// @notice Deposits a specified amount of staked NXM from the member into a staking pool.
364
  /// @dev    Only callable by the staking pool associated with the given poolId
365
  /// @param from    The member address from which the NXM is transferred.
366
  /// @param amount  The amount of NXM to deposit.
367
  /// @param poolId  The ID of the staking pool.
368
  function depositStakedNXM(address from, uint amount, uint poolId) external override onlyStakingPool(poolId) {
15✔
369
    stakingPoolNXMBalances[poolId].deposits += amount.toUint128();
14✔
370
    token.operatorTransfer(from, amount);
14✔
371
  }
372

373
  /// @notice Withdraws a specified amount of staked NXM and rewards from a staking pool to the member address
374
  /// @dev    Only callable by the staking pool associated with the given poolId
375
  /// @param to                 The address to which the NXM and rewards are transferred.
376
  /// @param stakeToWithdraw    The amount of staked NXM to withdraw.
377
  /// @param rewardsToWithdraw  The amount of rewards to withdraw.
378
  /// @param poolId             The ID of the staking pool.
379
  function withdrawNXMStakeAndRewards(
380
    address to,
381
    uint stakeToWithdraw,
382
    uint rewardsToWithdraw,
383
    uint poolId
384
  ) external override onlyStakingPool(poolId) {
3✔
385

386
    StakingPoolNXMBalances memory poolBalances = stakingPoolNXMBalances[poolId];
2✔
387

388
    poolBalances.deposits -= stakeToWithdraw.toUint128();
2✔
389
    poolBalances.rewards -= rewardsToWithdraw.toUint128();
2✔
390
    stakingPoolNXMBalances[poolId] = poolBalances;
2✔
391

392
    token.transfer(to, stakeToWithdraw + rewardsToWithdraw);
2✔
393
  }
394

395
  /// @notice Burns a specified amount of staked NXM from a staking pool.
396
  /// @dev    Only callable by the staking pool associated with the given poolId
397
  /// @param amount  The amount of staked NXM to burn.
398
  /// @param poolId  The ID of the staking pool.
399
  function burnStakedNXM(uint amount, uint poolId) external override onlyStakingPool(poolId) {
3✔
400
    stakingPoolNXMBalances[poolId].deposits -= amount.toUint128();
2✔
401
    token.burn(amount);
2✔
402
  }
403
}
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