• 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

92.49
/contracts/modules/governance/Governor.sol
1
// SPDX-License-Identifier: GPL-3.0-only
2

3
pragma solidity ^0.8.28;
4

5
import "../../abstract/Multicall.sol";
6
import "../../abstract/RegistryAware.sol";
7
import "../../interfaces/IGovernor.sol";
8
import "../../interfaces/IRegistry.sol";
9
import "../../interfaces/ITokenController.sol";
10
import "../../libraries/SafeUintCast.sol";
11

12
contract Governor is IGovernor, RegistryAware, Multicall {
13
  using SafeUintCast for uint;
14

15
  /* ========== storage ========== */
16

17
  uint public proposalCount;
18

19
  mapping(uint proposalId => Proposal) internal proposals;
20

21
  mapping(uint proposalId => string) internal descriptions;
22

23
  mapping(uint proposalId => Transaction[]) internal transactions;
24

25
  mapping(uint proposalId => Tally) internal tallies;
26

27
  mapping(uint proposalId => mapping(uint memberId => Vote)) internal votes;
28

29
  /* ========== immutables and constants ========== */
30

31
  ITokenController public immutable tokenController;
32

33
  uint public constant TIMELOCK_PERIOD = 1 days;
34
  uint public constant VOTING_PERIOD = 3 days;
35
  uint public constant ADVISORY_BOARD_THRESHOLD = 3;
36
  uint public constant MEMBER_VOTE_QUORUM_PERCENTAGE = 15; // 15% of token supply
37
  uint public constant PROPOSAL_THRESHOLD = 100 ether; // minimum 100 tokens to open an AB swap proposal
38
  uint public constant VOTE_WEIGHT_CAP_PERCENTAGE = 5; // 5%
39

40
  /* ========== logic ========== */
41

42
  constructor(address _registry) RegistryAware(_registry) {
43
    tokenController = ITokenController(fetch(C_TOKEN_CONTROLLER));
2✔
44
  }
45

46
  function _getVoteWeight(address voter) internal view returns (uint) {
47
    uint totalSupply = tokenController.totalSupply();
14✔
48
    uint weight = tokenController.totalBalanceOf(voter) + 1 ether;
14✔
49
    uint maxWeight = totalSupply * VOTE_WEIGHT_CAP_PERCENTAGE / 100;
14✔
50
    return weight > maxWeight ? maxWeight : weight;
14✔
51
  }
52

53
  function _lockTokenTransfers(address voter, uint deadline) internal {
54
    uint duration = deadline - block.timestamp;
4✔
55
    tokenController.lockForMemberVote(voter, duration);
4✔
56
  }
57

58
  function propose(
59
    Transaction[] calldata txs,
60
    string calldata description
61
  ) external {
62
    require(registry.isAdvisoryBoardMember(msg.sender), OnlyAdvisoryBoardMember());
24✔
63
    _propose(ProposalKind.AdvisoryBoard, txs, description);
23✔
64
  }
65

66
  function proposeAdvisoryBoardSwap(
67
    AdvisoryBoardSwap[] memory swaps,
68
    string calldata description
69
  ) external {
70

71
    require(registry.isMember(msg.sender), NotMember());
11✔
72

73
    // prevent spam
74
    uint weight = _getVoteWeight(msg.sender);
10✔
75
    require(weight > PROPOSAL_THRESHOLD, ProposalThresholdNotMet());
10✔
76

77
    Transaction[] memory txs = new Transaction[](swaps.length);
8✔
78

79
    for (uint i = 0; i < swaps.length; i++) {
8✔
80

81
      require(swaps[i].from != swaps[i].to, InvalidAdvisoryBoardSwap());
8✔
82
      require(swaps[i].from != 0 && swaps[i].to != 0, InvalidAdvisoryBoardSwap());
7✔
83

84
      txs[i] = Transaction({
5✔
85
        target: address(registry),
86
        value: 0,
87
        data: abi.encodeWithSelector(registry.swapAdvisoryBoardMember.selector, swaps[i].from, swaps[i].to)
88
      });
89
    }
90

91
    _propose(ProposalKind.Member, txs, description);
5✔
92
  }
93

94
  function _propose(
95
    ProposalKind kind,
96
    Transaction[] memory txs,
97
    string memory description
98
  ) internal {
99

100
    Proposal memory proposal = Proposal({
28✔
101
      kind: kind,
102
      proposedAt: block.timestamp.toUint32(),
103
      voteBefore: (block.timestamp + VOTING_PERIOD).toUint32(),
104
      executeAfter: (block.timestamp + VOTING_PERIOD + TIMELOCK_PERIOD).toUint32(),
105
      status: ProposalStatus.Proposed
106
    });
107

108
    uint proposalId = ++proposalCount;
28✔
109
    proposals[proposalId] = proposal;
28✔
110
    descriptions[proposalId] = description;
28✔
111

112
    for (uint i = 0; i < txs.length; i++) {
28✔
113
      transactions[proposalId].push(txs[i]);
31✔
114
    }
115

116
    emit ProposalCreated(proposalId, kind, description);
28✔
117
  }
118

119
  function cancel(uint proposalId) external {
120

121
    require(registry.isAdvisoryBoardMember(msg.sender), OnlyAdvisoryBoardMember());
15✔
122

123
    Proposal memory proposal = proposals[proposalId];
12✔
124
    require(proposal.proposedAt > 0, ProposalNotFound());
12✔
125
    require(proposal.kind == ProposalKind.AdvisoryBoard, CannotCancelMemberProposal());
11✔
126
    require(proposal.status != ProposalStatus.Executed, ProposalAlreadyExecuted());
10✔
127
    require(proposal.status != ProposalStatus.Canceled, ProposalIsCanceled());
9✔
128

129
    proposal.status = ProposalStatus.Canceled;
8✔
130
    proposals[proposalId] = proposal;
8✔
131

132
    emit ProposalCanceled(proposalId);
8✔
133
  }
134

135
  function vote(uint proposalId, Choice choice) external {
136

137
    Proposal memory proposal = proposals[proposalId];
59✔
138
    require(proposal.proposedAt > 0, ProposalNotFound());
59✔
139
    require(block.timestamp < proposal.voteBefore, VotePeriodHasEnded());
58✔
140
    require(proposal.status != ProposalStatus.Executed, ProposalAlreadyExecuted());
56!
141
    require(proposal.status != ProposalStatus.Canceled, ProposalIsCanceled());
56✔
142

143
    uint memberId = registry.getMemberId(msg.sender);
54✔
144
    require(memberId > 0, NotMember());
54✔
145
    require(votes[proposalId][memberId].weight == 0, AlreadyVoted());
53✔
146

147
    bool isAbProposal = proposal.kind == ProposalKind.AdvisoryBoard;
52✔
148
    uint voterId = isAbProposal
52✔
149
      ? registry.getAdvisoryBoardSeat(msg.sender)
52✔
150
      : memberId;
151
    require(voterId > 0, NotAuthorizedToVote());
52✔
152

153
    uint96 weight = (isAbProposal ? 1 : _getVoteWeight(msg.sender)).toUint96();
51✔
154
    votes[proposalId][memberId] = Vote({ choice: choice, weight: weight });
51✔
155

156
    if (choice == Choice.For) {
51✔
157
      tallies[proposalId].forVotes += weight;
46✔
158
    }
159

160
    if (choice == Choice.Against) {
51✔
161
      tallies[proposalId].againstVotes += weight;
4✔
162
    }
163

164
    if (choice == Choice.Abstain) {
51✔
165
      tallies[proposalId].abstainVotes += weight;
1✔
166
    }
167

168
    if (isAbProposal && tallies[proposalId].forVotes >= ADVISORY_BOARD_THRESHOLD) {
51✔
169
      // start the timelock if the AB proposal has met the threshold
170
      proposal.executeAfter = (block.timestamp + TIMELOCK_PERIOD).toUint32();
13✔
171
    }
172

173
    if(!isAbProposal) {
51✔
174
      _lockTokenTransfers(msg.sender, block.timestamp + VOTING_PERIOD + TIMELOCK_PERIOD);
4✔
175
    }
176

177
    emit VoteCast(proposalId, proposal.kind, voterId, choice, weight);
51✔
178
  }
179

180
  function _performCall(address target, uint value, bytes memory data, uint txIndex) internal {
181

182
    // if data is not empty - the target is assumed to be a contract
183
    require(data.length == 0 || target.code.length != 0, TargetIsNotAContract());
14✔
184

185
    (bool ok, bytes memory returndata) = target.call{value: value}(data);
12✔
186

187
    if (ok) {
12!
188
      return;
12✔
189
    }
190

191
    uint size = returndata.length;
×
192

193
    if (size == 0) {
×
194
      revert RevertedWithoutReason(txIndex);
×
195
    }
196

197
    // bubble up the revert reason
198
    assembly {
×
199
      revert(add(returndata, 0x20), size)
200
    }
201
  }
202

203
  function execute(uint proposalId) external payable {
204

205
    Proposal memory proposal = proposals[proposalId];
22✔
206
    require(proposal.proposedAt > 0, ProposalNotFound());
22✔
207
    require(proposal.status != ProposalStatus.Executed, ProposalAlreadyExecuted());
21✔
208
    require(proposal.status != ProposalStatus.Canceled, ProposalIsCanceled());
20✔
209

210
    bool isAbProposal = proposal.kind == ProposalKind.AdvisoryBoard;
18✔
211
    require(isAbProposal || registry.isMember(msg.sender), NotMember());
18✔
212
    require(!isAbProposal || registry.isAdvisoryBoardMember(msg.sender), OnlyAdvisoryBoardMember());
17✔
213
    require(block.timestamp > proposal.executeAfter, TimelockHasNotEnded());
16✔
214

215
    Tally memory tally = tallies[proposalId];
15✔
216
    require(tally.forVotes > tally.againstVotes, VoteTalliedAgainst());
15✔
217

218
    if (isAbProposal) {
14✔
219
      require(tally.forVotes >= ADVISORY_BOARD_THRESHOLD, VoteThresholdNotMet());
12✔
220
    } else {
221
      uint quorum = tokenController.totalSupply() * MEMBER_VOTE_QUORUM_PERCENTAGE / 100;
2✔
222
      uint totalVotes = tally.forVotes + tally.againstVotes + tally.abstainVotes;
2✔
223
      require(totalVotes >= quorum, VoteQuorumNotMet());
2✔
224
      // todo: check if a majority threshold is required
225
    }
226

227
    Transaction[] memory txs = transactions[proposalId];
12✔
228

229
    for (uint i = 0; i < txs.length; i++) {
12✔
230
      _performCall(txs[i].target, txs[i].value, txs[i].data, i);
14✔
231
    }
232

233
    proposal.status = ProposalStatus.Executed;
10✔
234
    proposals[proposalId] = proposal;
10✔
235

236
    emit ProposalExecuted(proposalId);
10✔
237
  }
238

239
  function getProposal(uint proposalId) external view returns (Proposal memory) {
240
    return proposals[proposalId];
16✔
241
  }
242

243
  function getProposalDescription(uint proposalId) external view returns (string memory) {
UNCOV
244
    return descriptions[proposalId];
×
245
  }
246

247
  function getProposalTransactions(uint proposalId) external view returns (Transaction[] memory) {
UNCOV
248
    return transactions[proposalId];
×
249
  }
250

251
  function getProposalTally(uint proposalId) external view returns (Tally memory) {
UNCOV
252
    return tallies[proposalId];
×
253
  }
254

255
  function getProposalWithDetails(uint proposalId) external view returns (
256
    Proposal memory,
257
    string memory,
258
    Transaction[] memory,
259
    Tally memory
260
  ) {
UNCOV
261
    return (
×
262
      proposals[proposalId],
263
      descriptions[proposalId],
264
      transactions[proposalId],
265
      tallies[proposalId]
266
    );
267
  }
268

269
  function getVote(uint proposalId, uint memberId) external view returns (Vote memory) {
UNCOV
270
    return votes[proposalId][memberId];
×
271
  }
272

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