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

NexusMutual / smart-contracts / #778

23 Jul 2024 01:54PM UTC coverage: 87.593% (+4.3%) from 83.289%
#778

push

MilGard91
Run compilation in the storage layout extraction script if invoked from cli (thx @fvictorio)

1023 of 1274 branches covered (80.3%)

Branch coverage included in aggregate %.

2987 of 3304 relevant lines covered (90.41%)

170.73 hits per line

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

98.94
/contracts/modules/assessment/IndividualClaims.sol
1
// SPDX-License-Identifier: GPL-3.0-only
2

3
pragma solidity ^0.8.18;
4

5
import "../../abstract/MasterAwareV2.sol";
6
import "../../interfaces/IAssessment.sol";
7
import "../../interfaces/ICover.sol";
8
import "../../interfaces/ICoverNFT.sol";
9
import "../../interfaces/IERC20Detailed.sol";
10
import "../../interfaces/IIndividualClaims.sol";
11
import "../../interfaces/INXMToken.sol";
12
import "../../interfaces/IPool.sol";
13
import "../../interfaces/IRamm.sol";
14
import "../../interfaces/ICoverProducts.sol";
15
import "../../libraries/Math.sol";
16
import "../../libraries/SafeUintCast.sol";
17

18
/// Provides a way for cover owners to submit claims and redeem payouts. It is an entry point to
19
/// the assessment process where the members of the mutual decide the outcome of claims.
20
contract IndividualClaims is IIndividualClaims, MasterAwareV2 {
21

22
  // 0-10000 bps (i.e. double decimal precision percentage)
23
  uint internal constant MIN_ASSESSMENT_DEPOSIT_DENOMINATOR = 10000;
24
  uint internal constant REWARD_DENOMINATOR = 10000;
25

26
  // Used in operations involving NXM tokens and divisions
27
  uint internal constant PRECISION = 10 ** 18;
28

29
  INXMToken public immutable nxm;
30
  ICoverNFT public immutable coverNFT;
31

32
  /* ========== STATE VARIABLES ========== */
33

34
  Configuration public override config;
35

36
  Claim[] public override claims;
37

38
  // Mapping from coverId to claimId used to check if a new claim can be submitted on the given
39
  // cover as long as the last submitted claim reached a final state.
40
  mapping(uint => ClaimSubmission) public lastClaimSubmissionOnCover;
41

42
  /* ========== CONSTRUCTOR ========== */
43

44
  constructor(address nxmAddress, address coverNFTAddress) {
45
    nxm = INXMToken(nxmAddress);
12✔
46
    coverNFT = ICoverNFT(coverNFTAddress);
12✔
47
  }
48

49
  /* ========== VIEWS ========== */
50

51
  function cover() internal view returns (ICover) {
52
    return ICover(internalContracts[uint(ID.CO)]);
252✔
53
  }
54

55
  function coverProducts() internal view returns (ICoverProducts) {
56
    return ICoverProducts(internalContracts[uint(ID.CP)]);
88✔
57
  }
58

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

63
  function pool() internal view returns (IPool) {
64
    return IPool(internalContracts[uint(ID.P1)]);
236✔
65
  }
66

67
  function ramm() internal view returns (IRamm) {
68
    return IRamm(internalContracts[uint(ID.RA)]);
32✔
69
  }
70

71
  function getClaimsCount() external override view returns (uint) {
72
    return claims.length;
3✔
73
  }
74

75
  /// Returns the required assessment deposit and total reward for a new claim
76
  ///
77
  /// @dev This view is meant to be used either by users or user interfaces to determine the
78
  /// minimum assessment deposit value of the submitClaim tx.
79
  ///
80
  /// @param requestedAmount  The amount that is claimed
81
  /// @param segmentPeriod    The cover period of the segment in days
82
  /// @param coverAsset      The asset in which the payout would be made
83
  function getAssessmentDepositAndReward(
84
    uint requestedAmount,
85
    uint segmentPeriod,
86
    uint coverAsset
87
  ) public view returns (uint, uint) {
88
    IPool poolContract = pool();
191✔
89

90
    uint nxmPriceInETH = poolContract.getInternalTokenPriceInAsset(0);
191✔
91
    uint nxmPriceInCoverAsset = coverAsset == 0
191✔
92
      ? nxmPriceInETH
93
      : poolContract.getInternalTokenPriceInAsset(coverAsset);
94

95
    // Calculate the expected payout in NXM using the NXM price at cover purchase time
96
    uint expectedPayoutInNXM = requestedAmount * PRECISION / nxmPriceInCoverAsset;
191✔
97

98
    // Determine the total rewards that should be minted for the assessors based on cover period
99
    uint totalRewardInNXM = Math.min(
191✔
100
      uint(config.maxRewardInNXMWad) * PRECISION,
101
      expectedPayoutInNXM * uint(config.rewardRatio) * segmentPeriod / 365 days / REWARD_DENOMINATOR
102
    );
103

104
    uint dynamicDeposit = totalRewardInNXM * nxmPriceInETH / PRECISION;
191✔
105
    uint minDeposit = 1 ether * uint(config.minAssessmentDepositRatio) /
191✔
106
      MIN_ASSESSMENT_DEPOSIT_DENOMINATOR;
107

108
    // If dynamicDeposit falls below minDeposit use minDeposit instead
109
    uint assessmentDepositInETH = Math.max(minDeposit, dynamicDeposit);
191✔
110

111
    return (assessmentDepositInETH, totalRewardInNXM);
191✔
112
  }
113

114
  /// Returns a Claim aggregated in a human-friendly format.
115
  ///
116
  /// @dev This view is meant to be used in user interfaces to get a claim in a format suitable for
117
  /// displaying all relevant information in as few calls as possible. See ClaimDisplay struct.
118
  ///
119
  /// @param id    Claim identifier for which the ClaimDisplay is returned
120
  function getClaimDisplay(uint id) internal view returns (ClaimDisplay memory) {
121
    Claim memory claim = claims[id];
22✔
122
    (IAssessment.Poll memory poll,,) = assessment().assessments(claim.assessmentId);
22✔
123

124
    ClaimStatus claimStatus = ClaimStatus.PENDING;
22✔
125
    PayoutStatus payoutStatus = PayoutStatus.PENDING;
22✔
126
    {
22✔
127
      // Determine the claims status
128
      if (block.timestamp >= poll.end) {
22✔
129
        if (poll.accepted > poll.denied) {
13✔
130
          claimStatus = ClaimStatus.ACCEPTED;
4✔
131
        } else {
132
          claimStatus = ClaimStatus.DENIED;
9✔
133
        }
134
      }
135

136
      // Determine the payout status
137
      if (claimStatus == ClaimStatus.ACCEPTED) {
22✔
138
        if (claim.payoutRedeemed) {
4✔
139
          payoutStatus = PayoutStatus.COMPLETE;
1✔
140
        } else {
141
          (,,uint8 payoutCooldownInDays,) = assessment().config();
3✔
142
          if (
3✔
143
            block.timestamp >= poll.end +
144
            uint(payoutCooldownInDays) * 1 days +
145
            uint(config.payoutRedemptionPeriodInDays) * 1 days
146
          ) {
147
            payoutStatus = PayoutStatus.UNCLAIMED;
1✔
148
          }
149
        }
150
      } else if (claimStatus == ClaimStatus.DENIED) {
18✔
151
        payoutStatus = PayoutStatus.DENIED;
9✔
152
      }
153
    }
154

155
    CoverData memory coverData = cover().coverData(claim.coverId);
22✔
156

157
    CoverSegment memory segment = cover().coverSegmentWithRemainingAmount(claim.coverId, claim.segmentId);
22✔
158

159
    uint segmentEnd = segment.start + segment.period;
22✔
160

161
    string memory assetSymbol;
22✔
162
    if (claim.coverAsset == 0) {
22✔
163
      assetSymbol = "ETH";
9✔
164
    } else {
165

166
      address assetAddress = pool().getAsset(claim.coverAsset).assetAddress;
13✔
167
      try IERC20Detailed(assetAddress).symbol() returns (string memory v) {
13✔
168
        assetSymbol = v;
13✔
169
      } catch {
170
        // return assetSymbol as an empty string and use claim.coverAsset instead in the UI
171
      }
172
    }
173

174
    return ClaimDisplay(
22✔
175
      id,
176
      coverData.productId,
177
      claim.coverId,
178
      claim.assessmentId,
179
      claim.amount,
180
      assetSymbol,
181
      claim.coverAsset,
182
      segment.start,
183
      segmentEnd,
184
      poll.start,
185
      poll.end,
186
      uint(claimStatus),
187
      uint(payoutStatus)
188
    );
189
  }
190

191
  /// Returns an array of claims aggregated in a human-friendly format.
192
  ///
193
  /// @dev This view is meant to be used in user interfaces to get claims in a format suitable for
194
  /// displaying all relevant information in as few calls as possible. It can be used to paginate
195
  /// claims by providing the following parameters:
196
  ///
197
  /// @param ids   Array of Claim ids which are returned as ClaimDisplay
198
  function getClaimsToDisplay (uint[] calldata ids)
199
  external view returns (ClaimDisplay[] memory) {
200
    ClaimDisplay[] memory claimDisplays = new ClaimDisplay[](ids.length);
6✔
201
    for (uint i = 0; i < ids.length; i++) {
6✔
202
      uint id = ids[i];
22✔
203
      claimDisplays[i] = getClaimDisplay(id);
22✔
204
    }
205
    return claimDisplays;
6✔
206
  }
207

208
  /* === MUTATIVE FUNCTIONS ==== */
209

210
  /// Submits a claim for assessment
211
  ///
212
  /// @dev This function requires an ETH assessment fee. See: getAssessmentDepositAndReward
213
  ///
214
  /// @param coverId          Cover identifier
215
  /// @param requestedAmount  The amount expected to be received at payout
216
  /// @param ipfsMetadata     An IPFS hash that stores metadata about the claim that is emitted as
217
  ///                         an event. It's required for proof of loss. If this string is empty,
218
  ///                         no event is emitted.
219
  function submitClaim(
220
    uint32 coverId,
221
    uint16 segmentId,
222
    uint96 requestedAmount,
223
    string calldata ipfsMetadata
224
  ) external payable override onlyMember whenNotPaused returns (Claim memory claim) {
225
    require(
93✔
226
      coverNFT.isApprovedOrOwner(msg.sender, coverId),
227
      "Only the owner or approved addresses can submit a claim"
228
    );
229
    return _submitClaim(coverId, segmentId, requestedAmount, ipfsMetadata, msg.sender);
91✔
230
  }
231

232
  function submitClaimFor(
233
    uint32 coverId,
234
    uint16 segmentId,
235
    uint96 requestedAmount,
236
    string calldata ipfsMetadata,
237
    address owner
238
  ) external payable override onlyInternal whenNotPaused returns (Claim memory claim){
239
    return _submitClaim(coverId, segmentId, requestedAmount, ipfsMetadata, owner);
×
240
  }
241

242
  function _submitClaim(
243
    uint32 coverId,
244
    uint16 segmentId,
245
    uint96 requestedAmount,
246
    string calldata ipfsMetadata,
247
    address owner
248
  ) internal returns (Claim memory) {
249
    {
91✔
250
      ClaimSubmission memory previousSubmission = lastClaimSubmissionOnCover[coverId];
91✔
251
      if (previousSubmission.exists) {
91✔
252
        uint80 assessmentId = claims[previousSubmission.claimId].assessmentId;
21✔
253
        IAssessment.Poll memory poll = assessment().getPoll(assessmentId);
21✔
254
        (,,uint8 payoutCooldownInDays,) = assessment().config();
21✔
255
        uint payoutCooldown = uint(payoutCooldownInDays) * 1 days;
21✔
256
        if (block.timestamp >= poll.end + payoutCooldown) {
21✔
257
          if (
19✔
258
            poll.accepted > poll.denied &&
259
            block.timestamp < uint(poll.end) +
260
            payoutCooldown +
261
            uint(config.payoutRedemptionPeriodInDays) * 1 days
262
          ) {
263
            revert("A payout can still be redeemed");
1✔
264
          }
265
        } else {
266
          revert("A claim is already being assessed");
2✔
267
        }
268
      }
269
      lastClaimSubmissionOnCover[coverId] = ClaimSubmission(uint80(claims.length), true);
88✔
270
    }
271

272
    ICoverProducts coverProductsContract = coverProducts();
88✔
273
    CoverData memory coverData = cover().coverData(coverId);
88✔
274
    CoverSegment memory segment = cover().coverSegmentWithRemainingAmount(coverId, segmentId);
88✔
275

276
    {
88✔
277
      (, ProductType memory productType) = coverProductsContract.getProductWithType(coverData.productId);
88✔
278

279
      require(
88✔
280
        productType.claimMethod == uint8(ClaimMethod.IndividualClaims),
281
        "Invalid claim method for this product type"
282
      );
283
      require(requestedAmount <= segment.amount, "Covered amount exceeded");
87✔
284
      require(block.timestamp > segment.start, "Cannot buy cover and submit claim in the same block");
86✔
285
      require(
85✔
286
        uint(segment.start) + uint(segment.period) + uint(segment.gracePeriod) > block.timestamp,
287
        "Cover is outside the grace period"
288
      );
289

290
      emit ClaimSubmitted(
82✔
291
        owner,         // user
292
        claims.length,      // claimId
293
        coverId,            // coverId
294
        coverData.productId // user
295
      );
296
    }
297

298
    (uint assessmentDepositInETH, uint totalRewardInNXM) = getAssessmentDepositAndReward(
82✔
299
      requestedAmount,
300
      segment.period,
301
      coverData.coverAsset
302
    );
303

304
    uint newAssessmentId = assessment().startAssessment(totalRewardInNXM, assessmentDepositInETH);
82✔
305

306
    Claim memory claim = Claim({
82✔
307
      assessmentId: SafeUintCast.toUint80(newAssessmentId),
308
      coverId: coverId,
309
      segmentId: segmentId,
310
      amount: requestedAmount,
311
      coverAsset: coverData.coverAsset,
312
      payoutRedeemed: false
313
    });
314
    claims.push(claim);
82✔
315

316
    if (bytes(ipfsMetadata).length > 0) {
82✔
317
      emit MetadataSubmitted(claims.length - 1, ipfsMetadata);
1✔
318
    }
319

320
    require(msg.value >= assessmentDepositInETH, "Assessment deposit is insufficient");
82✔
321
    if (msg.value > assessmentDepositInETH) {
80✔
322
      // Refund ETH excess back to the sender
323
      (
35✔
324
        bool refunded,
325
        /* bytes data */
326
      ) = owner.call{value: msg.value - assessmentDepositInETH}("");
327
      require(refunded, "Assessment deposit excess refund failed");
35✔
328
    }
329

330
    // Transfer the assessment deposit to the pool
331
    (
79✔
332
      bool transferSucceeded,
333
      /* bytes data */
334
    ) =  getInternalContractAddress(ID.P1).call{value: assessmentDepositInETH}("");
335
    require(transferSucceeded, "Assessment deposit transfer to pool failed");
79✔
336

337
    return claim;
78✔
338
  }
339

340
  /// Redeems payouts for accepted claims
341
  ///
342
  /// @dev Anyone can call this function, the payout always being transfered to the NFT owner.
343
  /// When the tokens are transfered the assessment deposit is also sent back.
344
  ///
345
  /// @param claimId  Claim identifier
346
  function redeemClaimPayout(uint104 claimId) external override whenNotPaused {
347
    Claim memory claim = claims[claimId];
46✔
348
    (
46✔
349
      IAssessment.Poll memory poll,
350
      /*uint128 totalAssessmentReward*/,
351
      uint assessmentDepositInETH
352
    ) = assessment().assessments(claim.assessmentId);
353

354
    require(block.timestamp >= poll.end, "The claim is still being assessed");
46✔
355
    require(poll.accepted > poll.denied, "The claim needs to be accepted");
44✔
356

357
    (,,uint8 payoutCooldownInDays,) = assessment().config();
35✔
358
    uint payoutCooldown = uint(payoutCooldownInDays) * 1 days;
35✔
359

360
    require(block.timestamp >= poll.end + payoutCooldown, "The claim is in cooldown period");
35✔
361

362
    require(
34✔
363
      block.timestamp < uint(poll.end) + payoutCooldown + uint(config.payoutRedemptionPeriodInDays) * 1 days,
364
      "The redemption period has expired"
365
    );
366

367
    require(!claim.payoutRedeemed, "Payout has already been redeemed");
33✔
368
    claims[claimId].payoutRedeemed = true;
32✔
369

370
    ramm().updateTwap();
32✔
371
    address payable coverOwner = payable(cover().burnStake(
32✔
372
      claim.coverId,
373
      claim.segmentId,
374
      claim.amount
375
    ));
376

377
    // Send payout in cover asset
378
    pool().sendPayout(claim.coverAsset, coverOwner, claim.amount, assessmentDepositInETH);
32✔
379

380
    emit ClaimPayoutRedeemed(coverOwner, claim.amount, claimId, claim.coverId);
32✔
381
  }
382

383
  /// Updates configurable aprameters through governance
384
  ///
385
  /// @param paramNames  An array of elements from UintParams enum
386
  /// @param values      An array of the new values, each one corresponding to the parameter
387
  ///                    from paramNames on the same position.
388
  function updateUintParameters(
389
    UintParams[] calldata paramNames,
390
    uint[] calldata values
391
  ) external override onlyGovernance {
392
    Configuration memory newConfig = config;
6✔
393
    for (uint i = 0; i < paramNames.length; i++) {
6✔
394
      if (paramNames[i] == UintParams.payoutRedemptionPeriodInDays) {
16✔
395
        newConfig.payoutRedemptionPeriodInDays = uint8(values[i]);
4✔
396
        continue;
4✔
397
      }
398
      if (paramNames[i] == UintParams.rewardRatio) {
12✔
399
        newConfig.rewardRatio = uint16(values[i]);
4✔
400
        continue;
4✔
401
      }
402
      if (paramNames[i] == UintParams.maxRewardInNXMWad) {
8✔
403
        newConfig.maxRewardInNXMWad = uint16(values[i]);
4✔
404
        continue;
4✔
405
      }
406
      if (paramNames[i] == UintParams.minAssessmentDepositRatio) {
4!
407
        newConfig.minAssessmentDepositRatio = uint16(values[i]);
4✔
408
        continue;
4✔
409
      }
410
    }
411
    config = newConfig;
6✔
412
  }
413

414
  /// @dev Updates internal contract addresses to the ones stored in master. This function is
415
  /// automatically called by the master contract when a contract is added or upgraded.
416
  function changeDependentContractAddress() external override {
417
    internalContracts[uint(ID.TC)] = master.getLatestAddress("TC");
17✔
418
    internalContracts[uint(ID.MR)] = master.getLatestAddress("MR");
17✔
419
    internalContracts[uint(ID.P1)] = master.getLatestAddress("P1");
17✔
420
    internalContracts[uint(ID.CO)] = master.getLatestAddress("CO");
17✔
421
    internalContracts[uint(ID.AS)] = master.getLatestAddress("AS");
17✔
422
    internalContracts[uint(ID.RA)] = master.getLatestAddress("RA");
17✔
423
    internalContracts[uint(ID.CP)] = master.getLatestAddress("CP");
17✔
424

425
    Configuration memory currentConfig = config;
17✔
426
    bool notInitialized = bytes32(
17✔
427
      abi.encodePacked(
428
        currentConfig.rewardRatio,
429
        currentConfig.maxRewardInNXMWad,
430
        currentConfig.minAssessmentDepositRatio,
431
        currentConfig.payoutRedemptionPeriodInDays
432
      )
433
    ) == bytes32(0);
434

435
    if (notInitialized) {
17✔
436
      // The minimum cover premium per year is 2.6%. 20% of the cover premium is: 2.6% * 20% = 0.52%
437
      config.rewardRatio = 130; // 1.3%
2✔
438
      config.maxRewardInNXMWad = 50; // 50 NXM
2✔
439
      config.minAssessmentDepositRatio = 500; // 5% i.e. 0.05 ETH assessment minimum flat fee
2✔
440
      config.payoutRedemptionPeriodInDays = 30; // days to redeem the payout
2✔
441
    }
442
  }
443
}
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