• 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

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

3
pragma solidity ^0.8.18;
4

5
import "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol";
6
import "@openzeppelin/contracts-v4/token/ERC20/extensions/draft-IERC20Permit.sol";
7
import "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol";
8

9
import "../../abstract/MasterAwareV2.sol";
10
import "../../interfaces/IAssessment.sol";
11
import "../../interfaces/ICover.sol";
12
import "../../interfaces/ICoverNFT.sol";
13
import "../../interfaces/ICoverProducts.sol";
14
import "../../interfaces/INXMToken.sol";
15
import "../../interfaces/IPool.sol";
16
import "../../interfaces/IYieldTokenIncidents.sol";
17
import "../../interfaces/IRamm.sol";
18
import "../../libraries/Math.sol";
19
import "../../libraries/SafeUintCast.sol";
20

21
/// Allows cover owners to redeem payouts from yield token depeg incidents. It is an entry point
22
/// to the assessment process where the members of the mutual decides the validity of the
23
/// submitted incident. At the moment incidents can only be submitted by the Advisory Board members
24
/// while all members are allowed to vote through Assessment.sol.
25
contract YieldTokenIncidents is IYieldTokenIncidents, MasterAwareV2 {
26

27
  // Ratios are defined between 0-10000 bps (i.e. double decimal precision percentage)
28
  uint internal constant REWARD_DENOMINATOR = 10000;
29
  uint internal constant INCIDENT_EXPECTED_PAYOUT_DENOMINATOR = 10000;
30
  uint internal constant INCIDENT_PAYOUT_DEDUCTIBLE_DENOMINATOR = 10000;
31

32
  // Used in operations involving NXM tokens and divisions
33
  uint internal constant PRECISION = 10 ** 18;
34

35
  INXMToken public immutable nxm;
36

37
  ICoverNFT public immutable coverNFT;
38

39
  /* ========== STATE VARIABLES ========== */
40

41
  Configuration public override config;
42

43
  Incident[] public override incidents;
44

45
  /* ========== CONSTANTS ========== */
46

47
  address constant public ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
48
  uint constant public ETH_ASSET_ID = 0;
49

50
  /* ========== CONSTRUCTOR ========== */
51

52
  constructor(address nxmAddress, address coverNFTAddress) {
53
    nxm = INXMToken(nxmAddress);
9✔
54
    coverNFT = ICoverNFT(coverNFTAddress);
9✔
55
  }
56

57
  /* ========== VIEWS ========== */
58

59
  /// @dev Returns the number of incidents.
60
  function getIncidentsCount() external override view returns (uint) {
61
    return incidents.length;
3✔
62
  }
63

64
  function getIncidentDisplay(uint id) internal view returns (IncidentDisplay memory) {
65
    Incident memory incident = incidents[id];
30✔
66
    (IAssessment.Poll memory poll,,) = assessment().assessments(incident.assessmentId);
30✔
67

68
    IncidentStatus incidentStatus;
30✔
69

70
    (,,uint payoutCooldownInDays,) = assessment().config();
30✔
71
    uint redeemableUntil = poll.end + (payoutCooldownInDays + config.payoutRedemptionPeriodInDays) * 1 days;
30✔
72

73
    // Determine the incidents status
74
    if (block.timestamp < poll.end) {
30✔
75
      incidentStatus = IncidentStatus.PENDING;
8✔
76
    } else if (poll.accepted > poll.denied) {
22✔
77
      if (block.timestamp > redeemableUntil) {
4✔
78
        incidentStatus = IncidentStatus.EXPIRED;
2✔
79
      } else {
80
        incidentStatus = IncidentStatus.ACCEPTED;
2✔
81
      }
82
    } else {
83
      incidentStatus = IncidentStatus.DENIED;
18✔
84
    }
85

86

87
    return IncidentDisplay(
30✔
88
      id,
89
      incident.productId,
90
      incident.priceBefore,
91
      incident.date,
92
      poll.start,
93
      poll.end,
94
      redeemableUntil,
95
      uint(incidentStatus)
96
    );
97
  }
98

99
  /// Returns an array of incidents aggregated in a human-friendly format.
100
  ///
101
  /// @dev This view is meant to be used in user interfaces to get incidents in a format suitable
102
  /// for displaying all relevant information in as few calls as possible. It can be used to
103
  /// paginate incidents by providing the following parameters:
104
  ///
105
  /// @param ids   Array of Incident ids which are returned as IncidentDisplay
106
  function getIncidentsToDisplay (uint104[] calldata ids)
107
  external view returns (IncidentDisplay[] memory) {
108
    IncidentDisplay[] memory incidentDisplays = new IncidentDisplay[](ids.length);
6✔
109
    for (uint i = 0; i < ids.length; i++) {
6✔
110
      uint104 id = ids[i];
30✔
111
      incidentDisplays[i] = getIncidentDisplay(id);
30✔
112
    }
113
    return incidentDisplays;
6✔
114
  }
115

116
  /* === MUTATIVE FUNCTIONS ==== */
117

118
  /// Submits an incident for assessment
119
  ///
120
  /// @param productId            Product identifier on which the incident occured
121
  /// @param priceBefore          The price of the token before the incident
122
  /// @param priceBefore          The price of the token before the incident
123
  /// @param date                 The date the incident occured
124
  /// @param expectedPayoutInNXM  The date the incident occured
125
  /// @param ipfsMetadata         An IPFS hash that stores metadata about the incident that is
126
  ///                             emitted as an event.
127
  function submitIncident(
128
    uint24 productId,
129
    uint96 priceBefore,
130
    uint32 date,
131
    uint expectedPayoutInNXM,
132
    string calldata ipfsMetadata
133
  ) external override onlyGovernance whenNotPaused {
134

135
    ICoverProducts coverProductsContract = coverProducts();
58✔
136
    (, ProductType memory productType) = coverProductsContract.getProductWithType(productId);
58✔
137

138
    require(
58✔
139
      productType.claimMethod == uint8(ClaimMethod.YieldTokenIncidents),
140
      "Invalid claim method for this product type"
141
    );
142

143
    // Determine the total rewards that should be minted for the assessors based on cover period
144
    uint totalReward = Math.min(
56✔
145
      uint(config.maxRewardInNXMWad) * PRECISION,
146
      expectedPayoutInNXM * uint(config.rewardRatio) / REWARD_DENOMINATOR
147
    );
148
    uint incidentId = incidents.length;
56✔
149
    uint80 assessmentId = SafeUintCast.toUint80(assessment().startAssessment(totalReward, 0));
56✔
150

151
    incidents.push(Incident(assessmentId, productId, date, priceBefore));
56✔
152

153
    if (bytes(ipfsMetadata).length > 0) {
56✔
154
      emit MetadataSubmitted(incidentId, ipfsMetadata);
4✔
155
    }
156

157
    emit IncidentSubmitted(msg.sender, incidentId, productId, expectedPayoutInNXM);
56✔
158
  }
159

160
  /// Redeems payouts for eligible covers matching an accepted incident
161
  ///
162
  /// @dev The function must be called during the redemption period.
163
  ///
164
  /// @param incidentId      Index of the incident
165
  /// @param coverId         Index of the cover to be redeemed
166
  /// @param segmentId       Index of the cover's segment that's elidgible for redemption
167
  /// @param depeggedTokens  The amount of depegged tokens to be swapped for the coverAsset
168
  /// @param payoutAddress   The addres where the payout must be sent to
169
  /// @param optionalParams  (Optional) Reserved for permit data which is still in draft phase.
170
  ///                        For tokens that already support it, use it by encoding the following
171
  ///                        values in this exact order: address owner, address spender,
172
  ///                        uint256 value, uint256 deadline, uint8 v , bytes32 r, bytes32 s
173
  function redeemPayout(
174
    uint104 incidentId,
175
    uint32 coverId,
176
    uint segmentId,
177
    uint depeggedTokens,
178
    address payable payoutAddress,
179
    bytes calldata optionalParams
180
  ) external override onlyMember whenNotPaused returns (uint, uint8) {
181
    require(
59✔
182
      coverNFT.isApprovedOrOwner(msg.sender, coverId),
183
      "Only the cover owner or approved addresses can redeem"
184
    );
185

186
    ICover coverContract = ICover(getInternalContractAddress(ID.CO));
54✔
187
    CoverData memory coverData = coverContract.coverData(coverId);
54✔
188
    Product memory product = coverProducts().getProduct(coverData.productId);
54✔
189

190
    uint payoutAmount;
54✔
191
    {
54✔
192
      CoverSegment memory coverSegment = coverContract.coverSegmentWithRemainingAmount(coverId, segmentId);
54✔
193

194
      require(
54✔
195
        coverSegment.start + coverSegment.period + coverSegment.gracePeriod >= block.timestamp,
196
        "Grace period has expired"
197
      );
198

199
      Incident memory incident =  incidents[incidentId];
52✔
200

201
      {
52✔
202
        IAssessment.Poll memory poll = assessment().getPoll(incident.assessmentId);
52✔
203

204
        require(
52✔
205
          poll.accepted > poll.denied,
206
          "The incident needs to be accepted"
207
        );
208

209
        (,,uint8 payoutCooldownInDays,) = assessment().config();
46✔
210
        require(
46✔
211
          block.timestamp >= poll.end + payoutCooldownInDays * 1 days,
212
          "The voting and cooldown periods must end"
213
        );
214

215
        require(
44✔
216
          block.timestamp < poll.end +
217
          payoutCooldownInDays * 1 days +
218
          config.payoutRedemptionPeriodInDays * 1 days,
219
          "The redemption period has expired"
220
        );
221
      }
222

223
      require(
43✔
224
        coverSegment.start + coverSegment.period >= incident.date,
225
        "Cover ended before the incident"
226
      );
227

228
      require(coverSegment.start < incident.date, "Cover started after the incident");
42✔
229

230
      require(coverData.productId == incident.productId, "Product id mismatch");
40✔
231

232
      require(block.timestamp > coverSegment.start, "Cannot buy cover and submit claim in the same block");
37!
233

234
      // Calculate the payout amount
235
      {
37✔
236
        uint deductiblePriceBefore =
37✔
237
          uint(incident.priceBefore)
238
          * config.payoutDeductibleRatio
239
          / INCIDENT_PAYOUT_DEDUCTIBLE_DENOMINATOR;
240

241
        uint coverAssetDecimals;
37✔
242

243
        // ETH doesn't have a price feed oracle
244
        if (coverData.coverAsset == ETH_ASSET_ID) {
37✔
245
          coverAssetDecimals = 18;
25✔
246
        } else {
247
          IPool _pool = pool();
12✔
248
          address assetAddress = _pool.getAsset(coverData.coverAsset).assetAddress;
12✔
249
          (/* aggregator */, coverAssetDecimals) = _pool.priceFeedOracle().assets(assetAddress);
12✔
250
        }
251

252
        payoutAmount = depeggedTokens * deductiblePriceBefore / (10 ** coverAssetDecimals);
37✔
253
      }
254

255
      require(payoutAmount <= coverSegment.amount, "Payout exceeds covered amount");
37✔
256
    }
257

258
    ramm().updateTwap();
36✔
259
    coverContract.burnStake(coverId, segmentId, payoutAmount);
36✔
260

261
    if (optionalParams.length > 0) { // Skip the permit call when it is not provided
36✔
262
      (
1✔
263
        address owner,
264
        address spender,
265
        uint256 value,
266
        uint256 deadline,
267
        uint8 v,
268
        bytes32 r,
269
        bytes32 s
270
      ) = abi.decode(optionalParams, (address, address, uint256, uint256, uint8, bytes32, bytes32));
271

272
      if (spender != address(0)) {
1!
273
        IERC20Permit(product.yieldTokenAddress).permit(owner, spender, value, deadline, v, r, s);
1✔
274
      }
275
    }
276

277
    SafeERC20.safeTransferFrom(
36✔
278
      IERC20(product.yieldTokenAddress),
279
      msg.sender,
280
      address(this),
281
      depeggedTokens
282
    );
283

284
    IPool(internalContracts[uint(IMasterAwareV2.ID.P1)]).sendPayout(
30✔
285
      coverData.coverAsset,
286
      payoutAddress,
287
      payoutAmount,
288
      0 // deposit
289
    );
290

291
    emit IncidentPayoutRedeemed(msg.sender, payoutAmount, incidentId, coverId);
30✔
292

293
    return (payoutAmount, coverData.coverAsset);
30✔
294
  }
295

296
  /// Withdraws an amount of any asset held by this contract to a destination address.
297
  ///
298
  /// @param asset        The ERC20 address of the asset that needs to be withdrawn.
299
  /// @param destination  The address where the assets are transfered.
300
  /// @param amount       The amount of assets that are need to be transfered.
301
  function withdrawAsset(
302
    address asset,
303
    address destination,
304
    uint amount
305
  ) external onlyGovernance {
306
    IERC20 token = IERC20(asset);
2✔
307
    uint balance = token.balanceOf(address(this));
2✔
308
    uint transferAmount = amount > balance ? balance : amount;
2✔
309
    SafeERC20.safeTransfer(token, destination, transferAmount);
2✔
310
  }
311

312
  /// Updates configurable parameters through governance
313
  ///
314
  /// @param paramNames  An array of elements from UintParams enum
315
  /// @param values      An array of the new values, each one corresponding to the parameter
316
  ///                    from paramNames on the same position.
317
  function updateUintParameters(
318
    UintParams[] calldata paramNames,
319
    uint[] calldata values
320
  ) external override onlyGovernance {
321
    Configuration memory newConfig = config;
5✔
322
    for (uint i = 0; i < paramNames.length; i++) {
5✔
323
      if (paramNames[i] == UintParams.payoutRedemptionPeriodInDays) {
12✔
324
        newConfig.payoutRedemptionPeriodInDays = uint8(values[i]);
3✔
325
        continue;
3✔
326
      }
327
      if (paramNames[i] == UintParams.payoutDeductibleRatio) {
9✔
328
        newConfig.payoutDeductibleRatio = uint16(values[i]);
3✔
329
        continue;
3✔
330
      }
331
      if (paramNames[i] == UintParams.maxRewardInNXMWad) {
6✔
332
        newConfig.maxRewardInNXMWad = uint16(values[i]);
3✔
333
        continue;
3✔
334
      }
335
      if (paramNames[i] == UintParams.rewardRatio) {
3!
336
        newConfig.rewardRatio = uint16(values[i]);
3✔
337
        continue;
3✔
338
      }
339
    }
340
    config = newConfig;
5✔
341
  }
342

343
  /* ========== DEPENDENCIES ========== */
344

345
  function pool() internal view returns (IPool) {
346
    return IPool(internalContracts[uint(ID.P1)]);
12✔
347
  }
348

349
  function coverProducts() internal view returns (ICoverProducts) {
350
    return ICoverProducts(internalContracts[uint(ID.CP)]);
112✔
351
  }
352

353
  function assessment() internal view returns (IAssessment) {
354
    return IAssessment(internalContracts[uint(ID.AS)]);
214✔
355
  }
356

357
  function cover() internal view returns (ICover) {
358
    return ICover(internalContracts[uint(ID.CO)]);
×
359
  }
360

361
  function ramm() internal view returns (IRamm) {
362
    return IRamm(internalContracts[uint(ID.RA)]);
36✔
363
  }
364

365
  /// @dev Updates internal contract addresses to the ones stored in master. This function is
366
  /// automatically called by the master contract when a contract is added or upgraded.
367
  function changeDependentContractAddress() external override {
368
    internalContracts[uint(ID.TC)] = master.getLatestAddress("TC");
16✔
369
    internalContracts[uint(ID.MR)] = master.getLatestAddress("MR");
16✔
370
    internalContracts[uint(ID.P1)] = master.getLatestAddress("P1");
16✔
371
    internalContracts[uint(ID.CO)] = master.getLatestAddress("CO");
16✔
372
    internalContracts[uint(ID.AS)] = master.getLatestAddress("AS");
16✔
373
    internalContracts[uint(ID.RA)] = master.getLatestAddress("RA");
16✔
374
    internalContracts[uint(ID.CP)] = master.getLatestAddress("CP");
16✔
375

376
    Configuration memory currentConfig = config;
16✔
377
    bool notInitialized = bytes32(
16✔
378
      abi.encodePacked(
379
        currentConfig.rewardRatio,
380
        currentConfig.payoutDeductibleRatio,
381
        currentConfig.payoutRedemptionPeriodInDays,
382
        currentConfig.maxRewardInNXMWad
383
      )
384
    ) == bytes32(0);
385

386
    if (notInitialized) {
16✔
387
      config.rewardRatio = 130; // 1.3%
2✔
388
      config.payoutDeductibleRatio = 9000; // 90%
2✔
389
      config.payoutRedemptionPeriodInDays = 30; // days to redeem the payout
2✔
390
      config.maxRewardInNXMWad = 50; // 50 NXM
2✔
391
    }
392
  }
393

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