@ensuro/core 2.8.0-beta2 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,6 +131,10 @@ Also the docker container is prepared to run [hardhat](https://hardhat.org/). Th
131
131
 
132
132
  - Audit by [Quantstamp](https://quantstamp.com/) - 2022-09-26 through 2022-10-20: [AuditReport](audits/Quantstamp-Ensuro-Final-Report-2022-11-09.pdf)
133
133
  - Audit by [SlowMist](https://www.slowmist.com) - 2021-09-29: [AuditReport](audits/SlowMistAuditReport-Ensuro-2021-09-29.pdf)
134
+ - Process Quality Review by [DefiSafety](https://www.defisafety.com/) - 2024-03-18: [Process Quality Review](audits/DefiSafety.Process_Quality_Review.Ensuro.pdf)
135
+
136
+ [![DeFiSafety Badge](audits/DefiSafety-93-badge.png)](https://www.defisafety.com/app/pqrs/594)
137
+
134
138
 
135
139
  ## Contributing
136
140
 
@@ -34,6 +34,51 @@ library Policy {
34
34
  uint40 expiration;
35
35
  }
36
36
 
37
+ struct PremiumComposition {
38
+ uint256 purePremium;
39
+ uint256 jrScr;
40
+ uint256 srScr;
41
+ uint256 jrCoc;
42
+ uint256 srCoc;
43
+ uint256 ensuroCommission;
44
+ uint256 partnerCommission;
45
+ uint256 totalPremium;
46
+ }
47
+
48
+ function getMinimumPremium(
49
+ IRiskModule.Params memory rmParams,
50
+ uint256 payout,
51
+ uint256 lossProb,
52
+ uint40 expiration,
53
+ uint40 start
54
+ ) internal pure returns (PremiumComposition memory minPremium) {
55
+ minPremium.purePremium = payout.wadMul(lossProb.wadMul(rmParams.moc));
56
+ minPremium.jrScr = payout.wadMul(rmParams.jrCollRatio);
57
+ if (minPremium.jrScr > minPremium.purePremium) {
58
+ minPremium.jrScr -= minPremium.purePremium;
59
+ } else {
60
+ minPremium.jrScr = 0;
61
+ }
62
+
63
+ minPremium.srScr = payout.wadMul(rmParams.collRatio);
64
+ if (minPremium.srScr > (minPremium.purePremium + minPremium.jrScr)) {
65
+ minPremium.srScr -= minPremium.purePremium + minPremium.jrScr;
66
+ } else {
67
+ minPremium.srScr = 0;
68
+ }
69
+
70
+ // Calculate CoCs
71
+ minPremium.jrCoc = minPremium.jrScr.wadMul((rmParams.jrRoc * (expiration - start)) / SECONDS_PER_YEAR);
72
+ minPremium.srCoc = minPremium.srScr.wadMul((rmParams.srRoc * (expiration - start)) / SECONDS_PER_YEAR);
73
+ uint256 totalCoc = minPremium.jrCoc + minPremium.srCoc;
74
+
75
+ minPremium.ensuroCommission =
76
+ minPremium.purePremium.wadMul(rmParams.ensuroPpFee) +
77
+ totalCoc.wadMul(rmParams.ensuroCocFee);
78
+
79
+ minPremium.totalPremium = minPremium.purePremium + minPremium.ensuroCommission + totalCoc;
80
+ }
81
+
37
82
  function initialize(
38
83
  IRiskModule riskModule,
39
84
  IRiskModule.Params memory rmParams,
@@ -52,27 +97,19 @@ library Policy {
52
97
  policy.lossProb = lossProb;
53
98
  policy.start = start;
54
99
  policy.expiration = expiration;
55
- policy.purePremium = payout.wadMul(lossProb.wadMul(rmParams.moc));
56
- // Calculate Junior and Senior SCR
57
- policy.jrScr = payout.wadMul(rmParams.jrCollRatio);
58
- if (policy.jrScr > policy.purePremium) {
59
- policy.jrScr -= policy.purePremium;
60
- } else {
61
- policy.jrScr = 0;
62
- }
63
- policy.srScr = payout.wadMul(rmParams.collRatio);
64
- if (policy.srScr > (policy.purePremium + policy.jrScr)) {
65
- policy.srScr -= policy.purePremium + policy.jrScr;
66
- } else {
67
- policy.srScr = 0;
68
- }
69
- // Calculate CoCs
70
- policy.jrCoc = policy.jrScr.wadMul((rmParams.jrRoc * (policy.expiration - policy.start)) / SECONDS_PER_YEAR);
71
- policy.srCoc = policy.srScr.wadMul((rmParams.srRoc * (policy.expiration - policy.start)) / SECONDS_PER_YEAR);
72
- uint256 coc = policy.jrCoc + policy.srCoc;
73
- policy.ensuroCommission = policy.purePremium.wadMul(rmParams.ensuroPpFee) + coc.wadMul(rmParams.ensuroCocFee);
74
- require((policy.purePremium + policy.ensuroCommission + coc) <= premium, "Premium less than minimum");
75
- policy.partnerCommission = premium - policy.purePremium - coc - policy.ensuroCommission;
100
+
101
+ PremiumComposition memory minPremium = getMinimumPremium(rmParams, payout, lossProb, expiration, start);
102
+
103
+ policy.purePremium = minPremium.purePremium;
104
+ policy.jrScr = minPremium.jrScr;
105
+ policy.srScr = minPremium.srScr;
106
+ policy.jrCoc = minPremium.jrCoc;
107
+ policy.srCoc = minPremium.srCoc;
108
+ policy.ensuroCommission = minPremium.ensuroCommission;
109
+
110
+ require(minPremium.totalPremium <= premium, "Premium less than minimum");
111
+
112
+ policy.partnerCommission = premium - minPremium.totalPremium;
76
113
  return policy;
77
114
  }
78
115
 
@@ -11,6 +11,7 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U
11
11
 
12
12
  import {IAccessManager} from "./interfaces/IAccessManager.sol";
13
13
  import {IEToken} from "./interfaces/IEToken.sol";
14
+ import {IPolicyHolderV2} from "./interfaces/IPolicyHolderV2.sol";
14
15
  import {IPolicyHolder} from "./interfaces/IPolicyHolder.sol";
15
16
  import {IPolicyPool} from "./interfaces/IPolicyPool.sol";
16
17
  import {IPolicyPoolComponent} from "./interfaces/IPolicyPoolComponent.sol";
@@ -43,6 +44,8 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
43
44
  bytes32 public constant LEVEL2_ROLE = keccak256("LEVEL2_ROLE");
44
45
  bytes32 public constant LEVEL3_ROLE = keccak256("LEVEL3_ROLE");
45
46
 
47
+ uint256 internal constant HOLDER_GAS_LIMIT = 150000;
48
+
46
49
  /**
47
50
  * @dev {AccessManager} that handles the access permissions for the PolicyPool and its components.
48
51
  */
@@ -125,6 +128,98 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
125
128
  */
126
129
  string internal _nftBaseURI;
127
130
 
131
+ /**
132
+ * @dev Constructor error when address(0) is sent as `access()`
133
+ */
134
+ error NoZeroAccess();
135
+
136
+ /**
137
+ * @dev Constructor error when address(0) is sent as `currency()`
138
+ */
139
+ error NoZeroCurrency();
140
+
141
+ /**
142
+ * @dev Constructor error (or setTreasury) when address(0) is sent as `treasury()`
143
+ */
144
+ error NoZeroTreasury();
145
+
146
+ /**
147
+ * @dev Initialization error when empty name for the ERC721 is sent
148
+ */
149
+ error NoEmptyName();
150
+
151
+ /**
152
+ * @dev Initialization error when empty symbol for the ERC721 is sent
153
+ */
154
+ error NoEmptySymbol();
155
+
156
+ /**
157
+ * @dev Upgrade error when the new implementation contract tries to change the `access()`
158
+ */
159
+ error UpgradeCannotChangeAccess();
160
+
161
+ /**
162
+ * @dev Upgrade error when the new implementation contract tries to change the `currency()`
163
+ */
164
+ error UpgradeCannotChangeCurrency();
165
+
166
+ /**
167
+ * @dev Error when trying to add a component that was already added to the PolicyPool
168
+ */
169
+ error ComponentAlreadyInThePool();
170
+
171
+ /**
172
+ * @dev Error when trying to add a component that isn't linked to this pool (`.policyPool() != this`)
173
+ */
174
+ error ComponentNotLinkedToThisPool();
175
+
176
+ /**
177
+ * @dev Error when a component is not of the right kind, it might happen if a component declared as
178
+ * ComponentKind.eToken doesn't support the IEToken interface (or similar) or when in a given operation
179
+ * we expect a component to be a risk module and the stored kind is different.
180
+ */
181
+ error ComponentNotTheRightKind(IPolicyPoolComponent component, ComponentKind expectedKind);
182
+
183
+ /**
184
+ * @dev Error when a component is expected to be deprecated for the operation (see `removeComponent`) and it isn't.
185
+ */
186
+ error ComponentNotDeprecated();
187
+
188
+ /**
189
+ * @dev Error when trying to remove a component that is still in use. The "in use" definition can change from one
190
+ * component to the other. For eToken in use means `totalSupply() != 0`. For PremiumsAccount means
191
+ * `purePremiums() != 0`. For RiskModule means `activeExposure() != 0`.
192
+ */
193
+ error ComponentInUseCannotRemove(ComponentKind kind, uint256 amount);
194
+
195
+ /**
196
+ * @dev Error when a component is not found in the pool (status = 0 = inactive)
197
+ */
198
+ error ComponentNotFound();
199
+
200
+ /**
201
+ * @dev Error when a component is not found in the pool or is not active (status != active)
202
+ */
203
+ error ComponentNotFoundOrNotActive();
204
+
205
+ /**
206
+ * @dev Error when a component is not active or deprecated. Happens on some operations like eToken withdrawals or
207
+ * policy resolutions that accept the component might be active or deprecated and isn't on any of those states.
208
+ */
209
+ error ComponentMustBeActiveOrDeprecated();
210
+
211
+ /**
212
+ * @dev Error when a method intented to be called by riskModule (and by policy's risk module) is called by someone
213
+ * else.
214
+ */
215
+ error OnlyRiskModuleAllowed();
216
+
217
+ /**
218
+ * @dev Error raised when IPolicyHolder doesn't return the expected selector answer when notified of policy payout,
219
+ * reception or replacement.
220
+ */
221
+ error InvalidNotificationResponse(bytes4 response);
222
+
128
223
  /**
129
224
  * @dev Event emitted when the treasury changes
130
225
  *
@@ -142,6 +237,13 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
142
237
  */
143
238
  event ComponentStatusChanged(IPolicyPoolComponent indexed component, ComponentKind kind, ComponentStatus newStatus);
144
239
 
240
+ /**
241
+ * @dev Event emitted when a IPolicyHolder reverts on the expiration notification. The operation doesn't reverts
242
+ *
243
+ * @param policyId The id of the policy being expired
244
+ * @param holder The address of the contract that owns the policy
245
+ */
246
+ event ExpirationNotificationFailed(uint256 indexed policyId, IPolicyHolder holder);
145
247
  /**
146
248
  * @dev Modifier that checks the caller has a given role
147
249
  */
@@ -167,8 +269,8 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
167
269
  */
168
270
  /// @custom:oz-upgrades-unsafe-allow constructor
169
271
  constructor(IAccessManager access_, IERC20Metadata currency_) {
170
- require(address(access_) != address(0), "PolicyPool: access cannot be zero address");
171
- require(address(currency_) != address(0), "PolicyPool: currency cannot be zero address");
272
+ if (address(access_) == address(0)) revert NoZeroAccess();
273
+ if (address(currency_) == address(0)) revert NoZeroCurrency();
172
274
  _disableInitializers();
173
275
  _access = access_;
174
276
  _currency = currency_;
@@ -182,8 +284,8 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
182
284
  * @param treasury_ The address of the treasury that will receive the protocol fees.
183
285
  */
184
286
  function initialize(string memory name_, string memory symbol_, address treasury_) public initializer {
185
- require(bytes(name_).length > 0, "PolicyPool: name cannot be empty");
186
- require(bytes(symbol_).length > 0, "PolicyPool: symbol cannot be empty");
287
+ if (bytes(name_).length == 0) revert NoEmptyName();
288
+ if (bytes(symbol_).length == 0) revert NoEmptySymbol();
187
289
  __UUPSUpgradeable_init();
188
290
  __ERC721_init(name_, symbol_);
189
291
  __Pausable_init();
@@ -204,8 +306,8 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
204
306
 
205
307
  function _authorizeUpgrade(address newImpl) internal view override onlyRole2(GUARDIAN_ROLE, LEVEL1_ROLE) {
206
308
  IPolicyPool newPool = IPolicyPool(newImpl);
207
- require(newPool.access() == _access, "Can't upgrade changing the access manager");
208
- require(newPool.currency() == _currency, "Can't upgrade changing the currency");
309
+ if (newPool.access() != _access) revert UpgradeCannotChangeAccess();
310
+ if (newPool.currency() != _currency) revert UpgradeCannotChangeCurrency();
209
311
  }
210
312
 
211
313
  /**
@@ -238,7 +340,7 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
238
340
  }
239
341
 
240
342
  function _setTreasury(address treasury_) internal {
241
- require(treasury_ != address(0), "PolicyPool: treasury cannot be the zero address");
343
+ if (treasury_ == address(0)) revert NoZeroTreasury();
242
344
  _treasury = treasury_;
243
345
  emit ComponentChanged(IAccessManager.GovernanceActions.setTreasury, _treasury);
244
346
  }
@@ -280,15 +382,14 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
280
382
  */
281
383
  function addComponent(IPolicyPoolComponent component, ComponentKind kind) external onlyRole(LEVEL1_ROLE) {
282
384
  Component storage comp = _components[component];
283
- require(comp.status == ComponentStatus.inactive, "Component already in the pool");
284
- require(component.policyPool() == this, "Component not linked to this pool");
385
+ if (comp.status != ComponentStatus.inactive) revert ComponentAlreadyInThePool();
386
+ if (component.policyPool() != this) revert ComponentNotLinkedToThisPool();
285
387
 
286
- require(
287
- (kind == ComponentKind.eToken && component.supportsInterface(type(IEToken).interfaceId)) ||
288
- (kind == ComponentKind.premiumsAccount && component.supportsInterface(type(IPremiumsAccount).interfaceId)) ||
289
- (kind == ComponentKind.riskModule && component.supportsInterface(type(IRiskModule).interfaceId)),
290
- "PolicyPool: Not the right kind"
291
- );
388
+ if (
389
+ (kind == ComponentKind.eToken && !component.supportsInterface(type(IEToken).interfaceId)) ||
390
+ (kind == ComponentKind.premiumsAccount && !component.supportsInterface(type(IPremiumsAccount).interfaceId)) ||
391
+ (kind == ComponentKind.riskModule && !component.supportsInterface(type(IRiskModule).interfaceId))
392
+ ) revert ComponentNotTheRightKind(component, kind);
292
393
 
293
394
  comp.status = ComponentStatus.active;
294
395
  comp.kind = kind;
@@ -321,14 +422,16 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
321
422
  */
322
423
  function removeComponent(IPolicyPoolComponent component) external onlyRole(LEVEL1_ROLE) {
323
424
  Component storage comp = _components[component];
324
- require(comp.status == ComponentStatus.deprecated, "Component not deprecated");
425
+ if (comp.status != ComponentStatus.deprecated) revert ComponentNotDeprecated();
325
426
  if (comp.kind == ComponentKind.eToken) {
326
- require(IEToken(address(component)).totalSupply() == 0, "EToken has liquidity, can't be removed");
427
+ if (IEToken(address(component)).totalSupply() != 0)
428
+ revert ComponentInUseCannotRemove(comp.kind, IEToken(address(component)).totalSupply());
327
429
  } else if (comp.kind == ComponentKind.riskModule) {
328
- require(IRiskModule(address(component)).activeExposure() == 0, "Can't remove a module with active policies");
430
+ if (IRiskModule(address(component)).activeExposure() != 0)
431
+ revert ComponentInUseCannotRemove(comp.kind, IRiskModule(address(component)).activeExposure());
329
432
  } else if (comp.kind == ComponentKind.premiumsAccount) {
330
433
  IPremiumsAccount pa = IPremiumsAccount(address(component));
331
- require(pa.purePremiums() == 0, "Can't remove a PremiumsAccount with premiums");
434
+ if (pa.purePremiums() != 0) revert ComponentInUseCannotRemove(comp.kind, pa.purePremiums());
332
435
  IEToken etk = pa.juniorEtk();
333
436
  if (address(etk) != address(0)) {
334
437
  etk.removeBorrower(address(pa));
@@ -360,13 +463,13 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
360
463
  ComponentStatus newStatus
361
464
  ) external onlyRole2(GUARDIAN_ROLE, LEVEL1_ROLE) {
362
465
  Component storage comp = _components[component];
363
- require(comp.status != ComponentStatus.inactive, "Component not found");
364
- require(
365
- (newStatus == ComponentStatus.active && _access.hasRole(LEVEL1_ROLE, _msgSender())) ||
366
- (newStatus == ComponentStatus.suspended && _access.hasRole(GUARDIAN_ROLE, _msgSender())) ||
367
- (newStatus == ComponentStatus.deprecated && _access.hasRole(LEVEL1_ROLE, _msgSender())),
368
- "Only GUARDIAN can suspend / Only LEVEL1 can activate/deprecate"
369
- );
466
+ if (comp.status == ComponentStatus.inactive) revert ComponentNotFound();
467
+ if (newStatus == ComponentStatus.active || newStatus == ComponentStatus.deprecated) {
468
+ _access.checkRole(LEVEL1_ROLE, _msgSender());
469
+ } else {
470
+ // ComponentStatus.suspended requires GUARDIAN_ROLE
471
+ _access.checkRole(GUARDIAN_ROLE, _msgSender());
472
+ }
370
473
  comp.status = newStatus;
371
474
  emit ComponentStatusChanged(component, comp.kind, newStatus);
372
475
  }
@@ -383,20 +486,18 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
383
486
 
384
487
  function _componentStatus(address component, ComponentKind kind) internal view returns (ComponentStatus) {
385
488
  Component storage comp = _components[IPolicyPoolComponent(component)];
386
- require(comp.kind == kind, "Component is not of the right kind");
489
+ if (comp.kind != kind) revert ComponentNotTheRightKind(IPolicyPoolComponent(component), kind);
387
490
  return comp.status;
388
491
  }
389
492
 
390
493
  function _requireCompActive(address component, ComponentKind kind) internal view {
391
- require(_componentStatus(component, kind) == ComponentStatus.active, "Component not found or not active");
494
+ if (_componentStatus(component, kind) != ComponentStatus.active) revert ComponentNotFoundOrNotActive();
392
495
  }
393
496
 
394
497
  function _requireCompActiveOrDeprecated(address component, ComponentKind kind) internal view {
395
498
  ComponentStatus status = _componentStatus(component, kind);
396
- require(
397
- status == ComponentStatus.active || status == ComponentStatus.deprecated,
398
- "Component must be active or deprecated"
399
- );
499
+ if (status != ComponentStatus.active && status != ComponentStatus.deprecated)
500
+ revert ComponentMustBeActiveOrDeprecated();
400
501
  }
401
502
 
402
503
  function deposit(IEToken eToken, uint256 amount) external override whenNotPaused {
@@ -419,7 +520,7 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
419
520
  ) external override whenNotPaused returns (uint256) {
420
521
  // Checks
421
522
  IRiskModule rm = policy.riskModule;
422
- require(address(rm) == _msgSender(), "Only the RM can create new policies");
523
+ if (address(rm) != _msgSender()) revert OnlyRiskModuleAllowed();
423
524
  _requireCompActive(address(rm), ComponentKind.riskModule);
424
525
  IPremiumsAccount pa = rm.premiumsAccount();
425
526
  _requireCompActive(address(pa), ComponentKind.premiumsAccount);
@@ -459,7 +560,7 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
459
560
  // Checks
460
561
  _validatePolicy(oldPolicy);
461
562
  IRiskModule rm = oldPolicy.riskModule;
462
- require(address(rm) == _msgSender(), "Only the RM can create new policies");
563
+ if (address(rm) != _msgSender()) revert OnlyRiskModuleAllowed();
463
564
  _requireCompActive(address(rm), ComponentKind.riskModule);
464
565
  IPremiumsAccount pa = rm.premiumsAccount();
465
566
  _requireCompActive(address(pa), ComponentKind.premiumsAccount);
@@ -509,6 +610,7 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
509
610
 
510
611
  emit NewPolicy(rm, newPolicy_);
511
612
  emit PolicyReplaced(rm, oldPolicy.id, newPolicy_.id);
613
+ _notifyReplacement(oldPolicy.id, newPolicy_.id);
512
614
  return newPolicy_.id;
513
615
  }
514
616
 
@@ -566,7 +668,7 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
566
668
  // Checks
567
669
  _validatePolicy(policy);
568
670
  IRiskModule rm = policy.riskModule;
569
- require(expired || address(rm) == _msgSender(), "Only the RM can resolve policies");
671
+ if (!expired && address(rm) != _msgSender()) revert OnlyRiskModuleAllowed();
570
672
  require(payout == 0 || policy.expiration > block.timestamp, "Can't pay expired policy");
571
673
  _requireCompActiveOrDeprecated(address(rm), ComponentKind.riskModule);
572
674
 
@@ -606,7 +708,7 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
606
708
  if (!ERC165Checker.supportsInterface(customer, type(IPolicyHolder).interfaceId)) return;
607
709
 
608
710
  bytes4 retval = IPolicyHolder(customer).onPayoutReceived(_msgSender(), address(this), policyId, payout);
609
- require(retval == IPolicyHolder.onPayoutReceived.selector, "PolicyPool: Invalid return value from IPolicyHolder");
711
+ if (retval != IPolicyHolder.onPayoutReceived.selector) revert InvalidNotificationResponse(retval);
610
712
  }
611
713
 
612
714
  /**
@@ -617,13 +719,29 @@ contract PolicyPool is IPolicyPool, PausableUpgradeable, UUPSUpgradeable, ERC721
617
719
  if (!AddressUpgradeable.isContract(customer)) return;
618
720
  if (!ERC165Checker.supportsInterface(customer, type(IPolicyHolder).interfaceId)) return;
619
721
 
620
- try IPolicyHolder(customer).onPolicyExpired(_msgSender(), address(this), policyId) returns (bytes4) {
722
+ try IPolicyHolder(customer).onPolicyExpired{gas: HOLDER_GAS_LIMIT}(_msgSender(), address(this), policyId) returns (
723
+ bytes4
724
+ ) {
621
725
  return;
622
726
  } catch {
727
+ emit ExpirationNotificationFailed(policyId, IPolicyHolder(customer));
623
728
  return;
624
729
  }
625
730
  }
626
731
 
732
+ /**
733
+ * @dev Notifies the replacement with a callback if the policyholder is a contract. Never reverts.
734
+ */
735
+ function _notifyReplacement(uint256 oldPolicyId, uint256 newPolicyId) internal {
736
+ address customer = ownerOf(oldPolicyId);
737
+ if (!AddressUpgradeable.isContract(customer)) return;
738
+ if (!ERC165Checker.supportsInterface(customer, type(IPolicyHolderV2).interfaceId)) return;
739
+
740
+ bytes4 retval = IPolicyHolderV2(customer).onPolicyReplaced(_msgSender(), address(this), oldPolicyId, newPolicyId);
741
+ // PolicyHolder can revert and cancel the policy replacement
742
+ if (retval != IPolicyHolderV2.onPolicyReplaced.selector) revert InvalidNotificationResponse(retval);
743
+ }
744
+
627
745
  /**
628
746
  * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
629
747
  * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
@@ -42,6 +42,9 @@ abstract contract PolicyPoolComponent is UUPSUpgradeable, PausableUpgradeable, I
42
42
  event GovernanceAction(IAccessManager.GovernanceActions indexed action, uint256 value);
43
43
  event ComponentChanged(IAccessManager.GovernanceActions indexed action, address value);
44
44
 
45
+ error NoZeroPolicyPool();
46
+ error UpgradeCannotChangePolicyPool();
47
+
45
48
  modifier onlyPolicyPool() {
46
49
  require(_msgSender() == address(_policyPool), "The caller must be the PolicyPool");
47
50
  _;
@@ -76,7 +79,7 @@ abstract contract PolicyPoolComponent is UUPSUpgradeable, PausableUpgradeable, I
76
79
 
77
80
  /// @custom:oz-upgrades-unsafe-allow constructor
78
81
  constructor(IPolicyPool policyPool_) {
79
- require(address(policyPool_) != address(0), "PolicyPoolComponent: policyPool cannot be zero address");
82
+ if (address(policyPool_) == address(0)) revert NoZeroPolicyPool();
80
83
  _disableInitializers();
81
84
  _policyPool = policyPool_;
82
85
  }
@@ -94,7 +97,7 @@ abstract contract PolicyPoolComponent is UUPSUpgradeable, PausableUpgradeable, I
94
97
  }
95
98
 
96
99
  function _upgradeValidations(address newImpl) internal view virtual {
97
- require(IPolicyPoolComponent(newImpl).policyPool() == _policyPool, "Can't upgrade changing the PolicyPool!");
100
+ if (IPolicyPoolComponent(newImpl).policyPool() != _policyPool) revert UpgradeCannotChangePolicyPool();
98
101
  }
99
102
 
100
103
  /**
@@ -56,12 +56,16 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
56
56
 
57
57
  address internal _wallet; // Address of the RiskModule provider
58
58
 
59
+ error NoZeroWallet();
60
+ error ExposureLimitCannotBeLessThanActiveExposure();
61
+ error PremiumsAccountMustBePartOfThePool();
62
+ error UpgradeCannotChangePremiumsAccount();
63
+
59
64
  /// @custom:oz-upgrades-unsafe-allow constructor
60
65
  constructor(IPolicyPool policyPool_, IPremiumsAccount premiumsAccount_) PolicyPoolComponent(policyPool_) {
61
- require(
62
- PolicyPoolComponent(address(premiumsAccount_)).policyPool() == policyPool_,
63
- "The PremiumsAccount must be part of the Pool"
64
- );
66
+ if (PolicyPoolComponent(address(premiumsAccount_)).policyPool() != policyPool_) {
67
+ revert PremiumsAccountMustBePartOfThePool();
68
+ }
65
69
  _premiumsAccount = premiumsAccount_;
66
70
  }
67
71
 
@@ -120,7 +124,9 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
120
124
  function _upgradeValidations(address newImpl) internal view virtual override {
121
125
  super._upgradeValidations(newImpl);
122
126
  IRiskModule newRM = IRiskModule(newImpl);
123
- require(newRM.premiumsAccount() == _premiumsAccount, "Can't upgrade changing the PremiumsAccount");
127
+ if (newRM.premiumsAccount() != _premiumsAccount) {
128
+ revert UpgradeCannotChangePremiumsAccount();
129
+ }
124
130
  }
125
131
 
126
132
  /**
@@ -133,8 +139,12 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
133
139
  // runs validation on RiskModule parameters
134
140
  function _validateParameters() internal view virtual override {
135
141
  // _maxPayoutPerPolicy no limits
136
- require(exposureLimit() >= _activeExposure, "Validation: exposureLimit can't be less than actual activeExposure");
137
- require(_wallet != address(0), "Validation: Wallet can't be zero address");
142
+ if (exposureLimit() < _activeExposure) {
143
+ revert ExposureLimitCannotBeLessThanActiveExposure();
144
+ }
145
+ if (_wallet == address(0)) {
146
+ revert NoZeroWallet();
147
+ }
138
148
  _validatePackedParams(_params);
139
149
  }
140
150
 
@@ -259,7 +269,9 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
259
269
  }
260
270
 
261
271
  function setWallet(address wallet_) external onlyComponentRole(RM_PROVIDER_ROLE) {
262
- require(wallet_ != address(0), "RiskModule: wallet cannot be the zero address");
272
+ if (wallet_ == address(0)) {
273
+ revert NoZeroWallet();
274
+ }
263
275
  _wallet = wallet_;
264
276
  _parameterChanged(IAccessManager.GovernanceActions.setWallet, uint256(uint160(wallet_)), false);
265
277
  }
@@ -269,32 +281,17 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
269
281
  uint256 lossProb,
270
282
  uint40 expiration
271
283
  ) public view virtual returns (uint256) {
272
- return _getMinimumPremium(payout, lossProb, expiration, params());
284
+ return _getMinimumPremium(payout, lossProb, expiration, uint40(block.timestamp), params());
273
285
  }
274
286
 
275
287
  function _getMinimumPremium(
276
288
  uint256 payout,
277
289
  uint256 lossProb,
278
290
  uint40 expiration,
291
+ uint40 start,
279
292
  Params memory p
280
- ) internal view returns (uint256) {
281
- uint256 purePremium = payout.wadMul(lossProb.wadMul(p.moc));
282
- uint256 jrScr = payout.wadMul(p.jrCollRatio);
283
- if (jrScr > purePremium) {
284
- jrScr -= purePremium;
285
- } else {
286
- jrScr = 0;
287
- }
288
- uint256 srScr = payout.wadMul(p.collRatio);
289
- if (srScr > (purePremium + jrScr)) {
290
- srScr -= purePremium + jrScr;
291
- } else {
292
- srScr = 0;
293
- }
294
- uint256 jrCoc = jrScr.wadMul((p.jrRoc * (expiration - block.timestamp)).wadDiv(SECONDS_IN_YEAR_WAD));
295
- uint256 srCoc = srScr.wadMul((p.srRoc * (expiration - block.timestamp)).wadDiv(SECONDS_IN_YEAR_WAD));
296
- uint256 ensuroCommission = purePremium.wadMul(p.ensuroPpFee) + (jrCoc + srCoc).wadMul(p.ensuroCocFee);
297
- return purePremium + ensuroCommission + (jrCoc + srCoc);
293
+ ) internal pure returns (uint256) {
294
+ return Policy.getMinimumPremium(p, payout, lossProb, expiration, start).totalPremium;
298
295
  }
299
296
 
300
297
  /**
@@ -334,11 +331,11 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
334
331
  uint96 internalId,
335
332
  Params memory params_
336
333
  ) internal returns (Policy.PolicyData memory policy) {
334
+ uint40 now_ = uint40(block.timestamp);
337
335
  if (premium == type(uint256).max) {
338
- premium = _getMinimumPremium(payout, lossProb, expiration, params_);
336
+ premium = _getMinimumPremium(payout, lossProb, expiration, now_, params_);
339
337
  }
340
338
  require(premium < payout, "Premium must be less than payout");
341
- uint40 now_ = uint40(block.timestamp);
342
339
  require(expiration > now_, "Expiration must be in the future");
343
340
  require(((expiration - now_) / 3600) < _params.maxDuration, "Policy exceeds max duration");
344
341
  require(onBehalfOf != address(0), "Customer can't be zero address");
@@ -381,7 +378,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
381
378
  Params memory params_
382
379
  ) internal virtual returns (Policy.PolicyData memory policy) {
383
380
  if (premium == type(uint256).max) {
384
- premium = _getMinimumPremium(payout, lossProb, expiration, params_);
381
+ premium = _getMinimumPremium(payout, lossProb, expiration, oldPolicy.start, params_);
385
382
  }
386
383
  require(premium < payout, "Premium must be less than payout");
387
384
  require(oldPolicy.expiration > uint40(block.timestamp), "Old policy is expired");
@@ -2,15 +2,16 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
5
+
5
6
  import {IPolicyPool} from "./interfaces/IPolicyPool.sol";
6
7
  import {IPremiumsAccount} from "./interfaces/IPremiumsAccount.sol";
7
- import {RiskModule} from "./RiskModule.sol";
8
8
  import {Policy} from "./Policy.sol";
9
+ import {RiskModule} from "./RiskModule.sol";
9
10
 
10
11
  /**
11
- * @title SignedQuote Risk Module
12
+ * @title SignedBucket Risk Module
12
13
  * @dev Risk Module that for policy creation verifies the different components of the price have been signed by a
13
- trusted account (PRICER_ROLE). One of the components of the price it's a bucket id that groups policies within
14
+ trusted account (PRICER_ROLE). One of the components of the price is a bucket id that groups policies within
14
15
  a risk module, with different parameters (such as collaterallization levels or fees).
15
16
  For the resolution (resolvePolicy), it has to be called by an authorized user
16
17
  * @custom:security-contact security@ensuro.co
@@ -18,12 +19,10 @@ import {Policy} from "./Policy.sol";
18
19
  */
19
20
  contract SignedBucketRiskModule is RiskModule {
20
21
  bytes32 public constant POLICY_CREATOR_ROLE = keccak256("POLICY_CREATOR_ROLE");
22
+ bytes32 public constant REPLACER_ROLE = keccak256("REPLACER_ROLE");
21
23
  bytes32 public constant PRICER_ROLE = keccak256("PRICER_ROLE");
22
24
  bytes32 public constant RESOLVER_ROLE = keccak256("RESOLVER_ROLE");
23
25
 
24
- /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
25
- bool internal immutable _creationIsOpen;
26
-
27
26
  mapping(uint256 => PackedParams) internal _buckets;
28
27
 
29
28
  /**
@@ -47,14 +46,12 @@ contract SignedBucketRiskModule is RiskModule {
47
46
  */
48
47
  event NewSignedPolicy(uint256 indexed policyId, bytes32 policyData);
49
48
 
49
+ error QuoteExpired();
50
+ error BucketCannotBeZero();
51
+ error BucketNotFound();
52
+
50
53
  /// @custom:oz-upgrades-unsafe-allow constructor
51
- constructor(
52
- IPolicyPool policyPool_,
53
- IPremiumsAccount premiumsAccount_,
54
- bool creationIsOpen_
55
- ) RiskModule(policyPool_, premiumsAccount_) {
56
- _creationIsOpen = creationIsOpen_;
57
- }
54
+ constructor(IPolicyPool policyPool_, IPremiumsAccount premiumsAccount_) RiskModule(policyPool_, premiumsAccount_) {}
58
55
 
59
56
  /**
60
57
  * @dev Initializes the RiskModule
@@ -89,9 +86,7 @@ contract SignedBucketRiskModule is RiskModule {
89
86
  bytes32 quoteSignatureVS,
90
87
  uint40 quoteValidUntil
91
88
  ) internal view {
92
- if (!_creationIsOpen)
93
- _policyPool.access().checkComponentRole(address(this), POLICY_CREATOR_ROLE, _msgSender(), false);
94
- require(quoteValidUntil >= block.timestamp, "Quote expired");
89
+ if (quoteValidUntil < block.timestamp) revert QuoteExpired();
95
90
 
96
91
  /**
97
92
  * Checks the quote has been signed by an authorized user
@@ -134,55 +129,6 @@ contract SignedBucketRiskModule is RiskModule {
134
129
  return createdPolicy;
135
130
  }
136
131
 
137
- /**
138
- * @dev Creates a new Policy using a signed quote. The caller is the payer of the policy. Returns all the struct, not just the id.
139
- *
140
- * Requirements:
141
- * - The caller approved the spending of the premium to the PolicyPool
142
- * - The quote has been signed by an address with the component role PRICER_ROLE
143
- *
144
- * Emits:
145
- * - {PolicyPool.NewPolicy}
146
- * - {NewSignedPolicy}
147
- *
148
- * @param payout The exposure (maximum payout) of the policy
149
- * @param premium The premium that will be paid by the payer
150
- * @param lossProb The probability of having to pay the maximum payout (wad)
151
- * @param expiration The expiration of the policy (timestamp)
152
- * @param onBehalfOf The policy holder
153
- * @param policyData A hash of the private details of the policy. The last 96 bits will be used as internalId
154
- * @param bucketId Identifies the group to which the policy belongs (that defines the RM parameters applicable to it)
155
- * @param quoteSignatureR The signature of the quote. R component (EIP-2098 signature)
156
- * @param quoteSignatureVS The signature of the quote. VS component (EIP-2098 signature)
157
- * @param quoteValidUntil The expiration of the quote
158
- * @return createdPolicy Returns the created policy
159
- */
160
- function newPolicyFull(
161
- uint256 payout,
162
- uint256 premium,
163
- uint256 lossProb,
164
- uint40 expiration,
165
- address onBehalfOf,
166
- bytes32 policyData,
167
- uint256 bucketId,
168
- bytes32 quoteSignatureR,
169
- bytes32 quoteSignatureVS,
170
- uint40 quoteValidUntil
171
- ) external whenNotPaused returns (Policy.PolicyData memory createdPolicy) {
172
- _checkSignature(
173
- payout,
174
- premium,
175
- lossProb,
176
- expiration,
177
- policyData,
178
- bucketId,
179
- quoteSignatureR,
180
- quoteSignatureVS,
181
- quoteValidUntil
182
- );
183
- return _newPolicySigned(payout, premium, lossProb, expiration, policyData, bucketId, _msgSender(), onBehalfOf);
184
- }
185
-
186
132
  /**
187
133
  * @dev Creates a new Policy using a signed quote. The caller is the payer of the policy.
188
134
  *
@@ -217,7 +163,7 @@ contract SignedBucketRiskModule is RiskModule {
217
163
  bytes32 quoteSignatureR,
218
164
  bytes32 quoteSignatureVS,
219
165
  uint40 quoteValidUntil
220
- ) external whenNotPaused returns (uint256) {
166
+ ) external whenNotPaused onlyComponentRole(POLICY_CREATOR_ROLE) returns (uint256) {
221
167
  _checkSignature(
222
168
  payout,
223
169
  premium,
@@ -233,21 +179,22 @@ contract SignedBucketRiskModule is RiskModule {
233
179
  }
234
180
 
235
181
  /**
236
- * @dev Creates a new Policy using a signed quote. The payer is the policy holder
182
+ * @dev Replace a policy with a new one, reusing the premium and the capital locked
237
183
  *
238
184
  * Requirements:
239
- * - currency().allowance(onBehalfOf, _msgSender()) > 0
185
+ * - The caller approved the spending of the premium to the PolicyPool
240
186
  * - The quote has been signed by an address with the component role PRICER_ROLE
187
+ * - The caller has been granted component role REPLACER_ROLE or creation is open
241
188
  *
242
189
  * Emits:
190
+ * - {PolicyPool.PolicyReplaced}
243
191
  * - {PolicyPool.NewPolicy}
244
- * - {NewSignedPolicy}
245
192
  *
246
- * @param payout The exposure (maximum payout) of the policy
247
- * @param premium The premium that will be paid by the payer
193
+ * @param oldPolicy The policy to be replaced
194
+ * @param payout The exposure (maximum payout) of the new policy
195
+ * @param premium The premium that will be paid by the caller
248
196
  * @param lossProb The probability of having to pay the maximum payout (wad)
249
197
  * @param expiration The expiration of the policy (timestamp)
250
- * @param onBehalfOf The policy holder
251
198
  * @param policyData A hash of the private details of the policy. The last 96 bits will be used as internalId
252
199
  * @param bucketId Identifies the group to which the policy belongs (that defines the RM parameters applicable to it)
253
200
  * @param quoteSignatureR The signature of the quote. R component (EIP-2098 signature)
@@ -255,32 +202,18 @@ contract SignedBucketRiskModule is RiskModule {
255
202
  * @param quoteValidUntil The expiration of the quote
256
203
  * @return Returns the id of the created policy
257
204
  */
258
- function newPolicyPaidByHolder(
205
+ function replacePolicy(
206
+ Policy.PolicyData calldata oldPolicy,
259
207
  uint256 payout,
260
208
  uint256 premium,
261
209
  uint256 lossProb,
262
210
  uint40 expiration,
263
- address onBehalfOf,
264
211
  bytes32 policyData,
265
212
  uint256 bucketId,
266
213
  bytes32 quoteSignatureR,
267
214
  bytes32 quoteSignatureVS,
268
215
  uint40 quoteValidUntil
269
- ) external whenNotPaused returns (uint256) {
270
- require(
271
- onBehalfOf == _msgSender() || currency().allowance(onBehalfOf, _msgSender()) > 0,
272
- "Sender is not authorized to create policies onBehalfOf"
273
- );
274
- /**
275
- * The standard is the payer should be the _msgSender() but usually, in this type of module,
276
- * the sender is an operative account managed by software, where the onBehalfOf is a more
277
- * secure account (hardware wallet) that does the cash movements.
278
- * This non standard behaviour allows for a more secure setup, where the sender never manages
279
- * cash.
280
- * We leverage the currency's allowance mechanism to allow the sender access to the payer's
281
- * funds.
282
- * Note that this allowance won't be spent, so anything above 0 is accepted.
283
- */
216
+ ) external whenNotPaused onlyComponentRole(REPLACER_ROLE) returns (uint256) {
284
217
  _checkSignature(
285
218
  payout,
286
219
  premium,
@@ -292,7 +225,10 @@ contract SignedBucketRiskModule is RiskModule {
292
225
  quoteSignatureVS,
293
226
  quoteValidUntil
294
227
  );
295
- return _newPolicySigned(payout, premium, lossProb, expiration, policyData, bucketId, onBehalfOf, onBehalfOf).id;
228
+ uint96 internalId = uint96(uint256(policyData) % 2 ** 96);
229
+ return
230
+ _replacePolicy(oldPolicy, payout, premium, lossProb, expiration, _msgSender(), internalId, bucketParams(bucketId))
231
+ .id;
296
232
  }
297
233
 
298
234
  function resolvePolicy(
@@ -316,7 +252,7 @@ contract SignedBucketRiskModule is RiskModule {
316
252
  uint256 bucketId,
317
253
  Params calldata params_
318
254
  ) external onlyGlobalOrComponentRole2(LEVEL1_ROLE, LEVEL2_ROLE) {
319
- require(bucketId != 0, "SignedBucketRiskModule: bucketId can't be zero, set default RM parameters");
255
+ if (bucketId == 0) revert BucketCannotBeZero();
320
256
  _buckets[bucketId] = PackedParams({
321
257
  moc: _wadTo4(params_.moc),
322
258
  jrCollRatio: _wadTo4(params_.jrCollRatio),
@@ -343,7 +279,8 @@ contract SignedBucketRiskModule is RiskModule {
343
279
  * @param bucketId Group identifier for the policies that will have these parameters
344
280
  */
345
281
  function deleteBucket(uint256 bucketId) external onlyGlobalOrComponentRole2(LEVEL1_ROLE, LEVEL2_ROLE) {
346
- require(bucketId != 0, "SignedBucketRiskModule: bucketId can't be zero, set default RM parameters");
282
+ if (bucketId == 0) revert BucketCannotBeZero();
283
+ if (_buckets[bucketId].moc == 0) revert BucketNotFound();
347
284
  delete _buckets[bucketId];
348
285
  emit BucketDeleted(bucketId);
349
286
  }
@@ -351,12 +288,12 @@ contract SignedBucketRiskModule is RiskModule {
351
288
  /**
352
289
  * @dev returns the risk bucket parameters for the given bucketId
353
290
  *
354
- * @param bucketId Id of the bucket of 0 if you want the default params
291
+ * @param bucketId Id of the bucket or 0 if you want the default params
355
292
  */
356
293
  function bucketParams(uint256 bucketId) public view returns (Params memory params_) {
357
294
  if (bucketId != 0) {
358
295
  PackedParams storage bucketParams_ = _buckets[bucketId];
359
- require(bucketParams_.moc != 0, "SignedBucketRiskModule: bucket not found!");
296
+ if (bucketParams_.moc == 0) revert BucketNotFound();
360
297
  params_ = _unpackParams(bucketParams_);
361
298
  } else {
362
299
  params_ = params();
@@ -377,7 +314,7 @@ contract SignedBucketRiskModule is RiskModule {
377
314
  uint40 expiration,
378
315
  uint256 bucketId
379
316
  ) public view virtual returns (uint256) {
380
- return _getMinimumPremium(payout, lossProb, expiration, bucketParams(bucketId));
317
+ return _getMinimumPremium(payout, lossProb, expiration, uint40(block.timestamp), bucketParams(bucketId));
381
318
  }
382
319
 
383
320
  /**
@@ -30,6 +30,8 @@ contract SignedQuoteRiskModule is RiskModule {
30
30
  */
31
31
  event NewSignedPolicy(uint256 indexed policyId, bytes32 policyData);
32
32
 
33
+ error QuoteExpired();
34
+
33
35
  /// @custom:oz-upgrades-unsafe-allow constructor
34
36
  constructor(
35
37
  IPolicyPool policyPool_,
@@ -75,7 +77,7 @@ contract SignedQuoteRiskModule is RiskModule {
75
77
  ) internal returns (Policy.PolicyData memory createdPolicy) {
76
78
  if (!_creationIsOpen)
77
79
  _policyPool.access().checkComponentRole(address(this), POLICY_CREATOR_ROLE, _msgSender(), false);
78
- require(quoteValidUntil >= block.timestamp, "Quote expired");
80
+ if (quoteValidUntil < block.timestamp) revert QuoteExpired();
79
81
 
80
82
  /**
81
83
  * Checks the quote has been signed by an authorized user
@@ -16,7 +16,7 @@ interface IPolicyHolder is IERC721Receiver {
16
16
  * No mather what's the return value or even if this function reverts, this function will not revert the policy
17
17
  * expiration.
18
18
  *
19
- * The selector can be obtained in Solidity with `IPolicyPool.onPolicyExpired.selector`.
19
+ * The selector can be obtained in Solidity with `IPolicyHolder.onPolicyExpired.selector`.
20
20
  */
21
21
  function onPolicyExpired(address operator, address from, uint256 policyId) external returns (bytes4);
22
22
 
@@ -27,7 +27,7 @@ interface IPolicyHolder is IERC721Receiver {
27
27
  * If interface is not implemented by the recipient, it will be ignored and the payout will be successful.
28
28
  * If any other value is returned or it reverts, the policy resolution / payout will be reverted.
29
29
  *
30
- * The selector can be obtained in Solidity with `IPolicyPool.onPayoutReceived.selector`.
30
+ * The selector can be obtained in Solidity with `IPolicyHolder.onPayoutReceived.selector`.
31
31
  */
32
32
  function onPayoutReceived(address operator, address from, uint256 policyId, uint256 amount) external returns (bytes4);
33
33
  }
@@ -0,0 +1,29 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IPolicyHolder} from "./IPolicyHolder.sol";
5
+
6
+ /**
7
+ * @title Policy holder interface - V2 of the interface that adds onPolicyReplaced endpoint
8
+ * @dev Interface for any contract that wants to be a holder of Ensuro Policies and receive notification of payouts and
9
+ * replacements.
10
+ *
11
+ * Implementors of this interface MUST return true on supportsInterface for both IPolicyHolder and IPolicyHolderV2.
12
+ */
13
+ interface IPolicyHolderV2 is IPolicyHolder {
14
+ /**
15
+ * @dev Whenever a policy is replaced, this function is called
16
+ *
17
+ * It must return its Solidity selector to confirm the payout.
18
+ * If interface is not implemented by the recipient, it will be ignored and the replacement will be successful.
19
+ * If any other value is returned or it reverts, the policy replacement will be reverted.
20
+ *
21
+ * The selector can be obtained in Solidity with `IPolicyHolderV2.onPolicyReplaced.selector`.
22
+ */
23
+ function onPolicyReplaced(
24
+ address operator,
25
+ address from,
26
+ uint256 oldPolicyId,
27
+ uint256 newPolicyId
28
+ ) external returns (bytes4);
29
+ }
@@ -2,22 +2,29 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import {IPolicyHolder} from "../interfaces/IPolicyHolder.sol";
5
+ import {IPolicyHolderV2} from "../interfaces/IPolicyHolderV2.sol";
5
6
  import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
6
7
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
8
+ import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
7
9
 
8
- contract PolicyHolderMock is IPolicyHolder {
10
+ contract PolicyHolderMock is IPolicyHolderV2 {
9
11
  uint256 public policyId;
10
12
  uint256 public payout;
11
13
  bool public fail;
14
+ bool public failReplace;
12
15
  bool public emptyRevert;
13
16
  bool public notImplemented;
14
17
  bool public badlyImplemented;
18
+ bool public badlyImplementedReplace;
15
19
  bool public noERC165;
20
+ bool public noV2;
21
+ uint256 public spendGasCount;
16
22
 
17
23
  enum NotificationKind {
18
24
  PolicyReceived,
19
25
  PayoutReceived,
20
- PolicyExpired
26
+ PolicyExpired,
27
+ PolicyReplaced
21
28
  }
22
29
 
23
30
  event NotificationReceived(NotificationKind kind, uint256 policyId, address operator, address from);
@@ -29,12 +36,21 @@ contract PolicyHolderMock is IPolicyHolder {
29
36
  emptyRevert = false;
30
37
  noERC165 = false;
31
38
  payout = type(uint256).max;
39
+ spendGasCount = 0;
32
40
  }
33
41
 
34
42
  function setFail(bool fail_) external {
35
43
  fail = fail_;
36
44
  }
37
45
 
46
+ function setFailReplace(bool failReplace_) external {
47
+ failReplace = failReplace_;
48
+ }
49
+
50
+ function setSpendGasCount(uint256 spendGasCount_) external {
51
+ spendGasCount = spendGasCount_;
52
+ }
53
+
38
54
  function setNotImplemented(bool notImplemented_) external {
39
55
  notImplemented = notImplemented_;
40
56
  }
@@ -43,6 +59,14 @@ contract PolicyHolderMock is IPolicyHolder {
43
59
  badlyImplemented = badlyImplemented_;
44
60
  }
45
61
 
62
+ function setBadlyImplementedReplace(bool badlyImplementedReplace_) external {
63
+ badlyImplementedReplace = badlyImplementedReplace_;
64
+ }
65
+
66
+ function setNoV2(bool noV2_) external {
67
+ noV2 = noV2_;
68
+ }
69
+
46
70
  function setNoERC165(bool noERC165_) external {
47
71
  noERC165 = noERC165_;
48
72
  }
@@ -51,6 +75,13 @@ contract PolicyHolderMock is IPolicyHolder {
51
75
  emptyRevert = emptyRevert_;
52
76
  }
53
77
 
78
+ function spendGas() internal {
79
+ // Spends gas doing storage writes
80
+ for (uint256 i = 0; i < spendGasCount; i++) {
81
+ StorageSlot.getUint256Slot(bytes32(100 + i)).value = i + 1;
82
+ }
83
+ }
84
+
54
85
  /**
55
86
  * @dev See {IERC165-supportsInterface}.
56
87
  */
@@ -60,7 +91,10 @@ contract PolicyHolderMock is IPolicyHolder {
60
91
  revert(0, 0)
61
92
  }
62
93
  if (notImplemented) return false;
63
- return interfaceId == type(IPolicyHolder).interfaceId || interfaceId == type(IERC165).interfaceId;
94
+ return
95
+ interfaceId == type(IPolicyHolder).interfaceId ||
96
+ (!noV2 && interfaceId == type(IPolicyHolderV2).interfaceId) ||
97
+ interfaceId == type(IERC165).interfaceId;
64
98
  }
65
99
 
66
100
  function onERC721Received(
@@ -82,6 +116,8 @@ contract PolicyHolderMock is IPolicyHolder {
82
116
 
83
117
  if (badlyImplemented) return bytes4(0x0badf00d);
84
118
 
119
+ spendGas();
120
+
85
121
  return IERC721Receiver.onERC721Received.selector;
86
122
  }
87
123
 
@@ -98,17 +134,13 @@ contract PolicyHolderMock is IPolicyHolder {
98
134
 
99
135
  if (badlyImplemented) return bytes4(0x0badf00d);
100
136
 
137
+ spendGas();
138
+
101
139
  return IPolicyHolder.onPolicyExpired.selector;
102
140
  }
103
141
 
104
142
  /**
105
- * @dev Whenever an Policy is resolved with payout > 0, this function is called
106
- *
107
- * It must return its Solidity selector to confirm the payout.
108
- * If interface is not implemented by the recipient, it will be ignored and the payout will be successful.
109
- * If any other value is returned or it reverts, the policy resolution / payout will be reverted.
110
- *
111
- * The selector can be obtained in Solidity with `IPolicyPool.onPayoutReceived.selector`.
143
+ * @dev See {IPolicyHolderV2-onPayoutReceived}.
112
144
  */
113
145
  function onPayoutReceived(
114
146
  address operator,
@@ -128,6 +160,35 @@ contract PolicyHolderMock is IPolicyHolder {
128
160
 
129
161
  if (badlyImplemented) return bytes4(0x0badf00d);
130
162
 
163
+ spendGas();
164
+
131
165
  return IPolicyHolder.onPayoutReceived.selector;
132
166
  }
167
+
168
+ /**
169
+ * @dev See {IPolicyHolderV2-onPolicyReplaced}.
170
+ */
171
+ function onPolicyReplaced(
172
+ address operator,
173
+ address from,
174
+ uint256 oldPolicyId,
175
+ uint256 newPolicyId
176
+ ) external override returns (bytes4) {
177
+ if (noV2) revert("Shouldn't call this method if V2 not enabled");
178
+ if (failReplace)
179
+ if (emptyRevert)
180
+ assembly {
181
+ revert(0, 0)
182
+ }
183
+ else revert("onPolicyReplaced: They told me I have to fail");
184
+ policyId = oldPolicyId;
185
+ payout = newPolicyId;
186
+ emit NotificationReceived(NotificationKind.PolicyReplaced, oldPolicyId, operator, from);
187
+
188
+ if (badlyImplementedReplace) return bytes4(0x0badf00d);
189
+
190
+ spendGas();
191
+
192
+ return IPolicyHolderV2.onPolicyReplaced.selector;
193
+ }
133
194
  }
package/js/utils.js CHANGED
@@ -144,22 +144,27 @@ async function grantRole(hre, contract, role, user, txOverrides = {}, log = () =
144
144
  * @param {Interface} interface The interface of the contract that contains the requested event
145
145
  * @param {TransactionReceipt} receipt Transaction receipt containing the events in the logs
146
146
  * @param {String} eventName The name of the event we are interested in
147
+ * @param {Boolean} firstOnly If false, returns all the events matching the name
148
+ * @param {String} contractAddress If not null, if returns only those events generated by the specified contract
147
149
  * @returns {LogDescription}
148
150
  */
149
- function getTransactionEvent(interface, receipt, eventName) {
151
+ function getTransactionEvent(interface, receipt, eventName, firstOnly = true, contractAddress = null) {
152
+ const ret = [];
150
153
  // for each log in the transaction receipt
151
154
  for (const log of receipt.logs) {
152
155
  let parsedLog;
156
+ if (contractAddress !== null && contractAddress !== log.address) continue;
153
157
  try {
154
158
  parsedLog = interface.parseLog(log);
155
159
  } catch (error) {
156
160
  continue;
157
161
  }
158
162
  if (parsedLog?.name == eventName) {
159
- return parsedLog;
163
+ if (firstOnly) return parsedLog;
164
+ ret.push(parsedLog);
160
165
  }
161
166
  }
162
- return null; // not found
167
+ return firstOnly ? null : ret;
163
168
  }
164
169
 
165
170
  /**
@@ -246,7 +251,7 @@ async function defaultPolicyParams(
246
251
  _A = amountFunction(6)
247
252
  ) {
248
253
  const now = await helpers.time.latest();
249
- const rmAddress = rm ? await ethers.resolveAddress(rm) : rm;
254
+ const rmAddress = rm ? getAddress(rm) : null;
250
255
  return {
251
256
  rmAddress,
252
257
  payout: payout || _A(1000),
@@ -295,6 +300,7 @@ module.exports = {
295
300
  amountFunction,
296
301
  defaultPolicyParams,
297
302
  defaultBucketParams,
303
+ getAddress,
298
304
  getComponentRole,
299
305
  getDefaultSigner,
300
306
  getRole,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ensuro/core",
3
3
  "description": "Ensuro - Decentralized insurance protocol",
4
- "version": "2.8.0-beta2",
4
+ "version": "2.8.1",
5
5
  "files": [
6
6
  "**/*.sol",
7
7
  "/build/contracts/*.json",
@@ -1,159 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- pragma solidity ^0.8.0;
3
-
4
- import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
5
- import {IPolicyPool} from "./interfaces/IPolicyPool.sol";
6
- import {IPremiumsAccount} from "./interfaces/IPremiumsAccount.sol";
7
- import {SignedQuoteRiskModule} from "./SignedQuoteRiskModule.sol";
8
- import {Policy} from "./Policy.sol";
9
-
10
- /**
11
- * @title Tiered SignedQuote Risk Module
12
- * @dev Risk Module that for policy creation verifies the different components of the price have been signed by a
13
- * trusted account (PRICER_ROLE). For the resolution (resolvePolicy), it has to be called by an authorized user.
14
- *
15
- * It allows different collaterallization levels for different policy types, by defining tiers or buckets of loss
16
- * probability.
17
- *
18
- * Each bucket's loss probability represents the upper bound of the bucket (inclusive). The global parameters of
19
- * the contract represent the 100% bucket.
20
- *
21
- * @custom:security-contact security@ensuro.co
22
- * @author Ensuro
23
- */
24
- contract TieredSignedQuoteRiskModule is SignedQuoteRiskModule {
25
- using SafeCast for uint256;
26
-
27
- uint8 public constant MAX_BUCKETS = 4;
28
-
29
- struct PackedBuckets {
30
- uint64[MAX_BUCKETS] lossProbs;
31
- }
32
-
33
- PackedBuckets internal _buckets;
34
- PackedParams[MAX_BUCKETS] internal _bucketParams;
35
-
36
- /**
37
- * @dev Emitted when a new risk bucket is created.
38
- * @param lossProb The loss probability of the new bucket.
39
- * @param params The packed parameters of the new bucket.
40
- */
41
- event NewBucket(uint256 lossProb, Params params);
42
-
43
- /**
44
- * @dev Emitted when the risks buckets are reset and only the default one remains
45
- */
46
- event BucketsReset();
47
-
48
- /// @custom:oz-upgrades-unsafe-allow constructor
49
- constructor(
50
- IPolicyPool policyPool_,
51
- IPremiumsAccount premiumsAccount_,
52
- bool creationIsOpen_
53
- ) SignedQuoteRiskModule(policyPool_, premiumsAccount_, creationIsOpen_) {} // solhint-disable-line no-empty-blocks
54
-
55
- /**
56
- * @dev Adds a new risk bucket with the given loss probability and parameters after the last one.
57
- *
58
- * Requirements:
59
- *
60
- * - The caller must have the LEVEL1_ROLE or LEVEL2_ROLE
61
- *
62
- * @param lossProb The loss probability of the new bucket.
63
- * @param params_ The parameters of the new bucket.
64
- */
65
- function pushBucket(
66
- uint256 lossProb,
67
- Params calldata params_
68
- ) external onlyGlobalOrComponentRole2(LEVEL1_ROLE, LEVEL2_ROLE) {
69
- uint256 newBucket = 0;
70
- if (_buckets.lossProbs[0] != 0) {
71
- // solhint-disable-next-line no-empty-blocks
72
- for (newBucket = 1; newBucket < MAX_BUCKETS && _buckets.lossProbs[newBucket] != 0; newBucket++) {}
73
- require(newBucket < MAX_BUCKETS, "No more than 4 buckets accepted");
74
- require(lossProb > uint256(_buckets.lossProbs[newBucket - 1]), "lossProb <= last lossProb - reset instead");
75
- }
76
- _buckets.lossProbs[newBucket] = lossProb.toUint64();
77
- _bucketParams[newBucket] = PackedParams({
78
- moc: _wadTo4(params_.moc),
79
- jrCollRatio: _wadTo4(params_.jrCollRatio),
80
- collRatio: _wadTo4(params_.collRatio),
81
- ensuroPpFee: _wadTo4(params_.ensuroPpFee),
82
- ensuroCocFee: _wadTo4(params_.ensuroCocFee),
83
- jrRoc: _wadTo4(params_.jrRoc),
84
- srRoc: _wadTo4(params_.srRoc),
85
- maxPayoutPerPolicy: type(uint32).max, // unused, but needs to be > 0
86
- exposureLimit: type(uint32).max, //unused, but needs to be > 0
87
- maxDuration: type(uint16).max //unused
88
- });
89
- if (newBucket < MAX_BUCKETS - 1) {
90
- _buckets.lossProbs[newBucket + 1] = 0;
91
- }
92
- _validatePackedParams(_bucketParams[newBucket]);
93
- emit NewBucket(lossProb, params_);
94
- }
95
-
96
- function resetBuckets() external onlyGlobalOrComponentRole2(LEVEL1_ROLE, LEVEL2_ROLE) {
97
- _buckets.lossProbs[0] = 0;
98
- emit BucketsReset();
99
- }
100
-
101
- /**
102
- * @dev returns the risk bucket parameters for the given loss probability
103
- */
104
- function bucketParams(uint256 lossProb) public view returns (Params memory) {
105
- for (uint256 i = 0; i < MAX_BUCKETS && _buckets.lossProbs[i] != 0; i++) {
106
- if (lossProb <= _buckets.lossProbs[i]) {
107
- return _unpackParams(_bucketParams[i]);
108
- }
109
- }
110
- return this.params();
111
- }
112
-
113
- function _newPolicy(
114
- uint256 payout,
115
- uint256 premium,
116
- uint256 lossProb,
117
- uint40 expiration,
118
- address payer,
119
- address onBehalfOf,
120
- uint96 internalId
121
- ) internal override whenNotPaused returns (Policy.PolicyData memory) {
122
- return
123
- _newPolicyWithParams(
124
- payout,
125
- premium,
126
- lossProb,
127
- expiration,
128
- payer,
129
- onBehalfOf,
130
- internalId,
131
- bucketParams(lossProb)
132
- );
133
- }
134
-
135
- function getMinimumPremium(
136
- uint256 payout,
137
- uint256 lossProb,
138
- uint40 expiration
139
- ) public view virtual override returns (uint256) {
140
- return _getMinimumPremium(payout, lossProb, expiration, bucketParams(lossProb));
141
- }
142
-
143
- /**
144
- * @dev returns the current risk bucket limits
145
- */
146
- function buckets() external view returns (uint256[MAX_BUCKETS] memory result) {
147
- for (uint256 i = 0; i < MAX_BUCKETS && _buckets.lossProbs[i] > 0; i++) {
148
- result[i] = _buckets.lossProbs[i];
149
- }
150
- return result;
151
- }
152
-
153
- /**
154
- * @dev This empty reserved space is put in place to allow future versions to add new
155
- * variables without shifting down storage in the inheritance chain.
156
- * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
157
- */
158
- uint256[45] private __gap;
159
- }