@ensuro/core 2.0.0-beta8 → 2.0.0

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.
Files changed (65) hide show
  1. package/README.md +31 -14
  2. package/build/contracts/AccessManager.sol/AccessManager.json +7 -2
  3. package/build/contracts/ERC4626AssetManager.sol/ERC4626AssetManager.json +28 -3
  4. package/build/contracts/EToken.sol/EToken.json +21 -15
  5. package/build/contracts/LPManualWhitelist.sol/LPManualWhitelist.json +21 -2
  6. package/build/contracts/LiquidityThresholdAssetManager.sol/LiquidityThresholdAssetManager.json +26 -1
  7. package/build/contracts/Policy.sol/Policy.json +2 -2
  8. package/build/contracts/PolicyPool.sol/PolicyPool.json +40 -2
  9. package/build/contracts/PolicyPoolComponent.sol/PolicyPoolComponent.json +19 -0
  10. package/build/contracts/PremiumsAccount.sol/PremiumsAccount.json +21 -15
  11. package/build/contracts/Reserve.sol/Reserve.json +19 -13
  12. package/build/contracts/RiskModule.sol/RiskModule.json +19 -0
  13. package/build/contracts/SignedQuoteRiskModule.sol/SignedQuoteRiskModule.json +1110 -0
  14. package/build/contracts/TimeScaled.sol/TimeScaled.json +3 -17
  15. package/build/contracts/TrustfulRiskModule.sol/TrustfulRiskModule.json +137 -2
  16. package/build/contracts/{WadRayMath.sol → dependencies/WadRayMath.sol}/WadRayMath.json +3 -3
  17. package/build/contracts/interfaces/IAssetManager.sol/IAssetManager.json +26 -1
  18. package/build/contracts/interfaces/IPolicyPool.sol/IPolicyPool.json +38 -0
  19. package/build/contracts/interfaces/IPolicyPoolComponent.sol/IPolicyPoolComponent.json +19 -0
  20. package/build/contracts/mocks/FixedRateVault.sol/FixedRateVault.json +2 -2
  21. package/build/contracts/mocks/ForwardProxy.sol/ForwardProxy.json +2 -2
  22. package/build/contracts/mocks/InterfaceIdCalculator.sol/InterfaceIdCalculator.json +180 -0
  23. package/build/contracts/mocks/PolicyHolderMock.sol/PolicyHolderMock.json +28 -2
  24. package/build/contracts/mocks/PolicyPoolComponentMock.sol/PolicyPoolComponentMock.json +21 -2
  25. package/build/contracts/mocks/PolicyPoolMock.sol/PolicyPoolMock.json +115 -2
  26. package/build/contracts/mocks/PolicyPoolMock.sol/PolicyPoolMockForward.json +2 -2
  27. package/build/contracts/mocks/RiskModuleMock.sol/RiskModuleMock.json +111 -2
  28. package/build/contracts/mocks/TestCurrency.sol/TestCurrency.json +2 -2
  29. package/build/contracts/mocks/TestNFT.sol/TestNFT.json +2 -2
  30. package/contracts/AccessManager.sol +31 -2
  31. package/contracts/ERC4626AssetManager.sol +8 -2
  32. package/contracts/EToken.sol +129 -60
  33. package/contracts/LPManualWhitelist.sol +18 -2
  34. package/contracts/LiquidityThresholdAssetManager.sol +28 -12
  35. package/contracts/Policy.sol +5 -3
  36. package/contracts/PolicyPool.sol +221 -29
  37. package/contracts/PolicyPoolComponent.sol +36 -25
  38. package/contracts/PremiumsAccount.sol +220 -32
  39. package/contracts/Reserve.sol +130 -19
  40. package/contracts/RiskModule.sol +53 -20
  41. package/contracts/SignedQuoteRiskModule.sol +310 -0
  42. package/contracts/TimeScaled.sol +19 -15
  43. package/contracts/TrustfulRiskModule.sol +107 -6
  44. package/contracts/dependencies/WadRayMath.sol +126 -0
  45. package/contracts/interfaces/IAccessManager.sol +68 -13
  46. package/contracts/interfaces/IAssetManager.sol +69 -2
  47. package/contracts/interfaces/IEToken.sol +4 -1
  48. package/contracts/interfaces/ILPWhitelist.sol +17 -0
  49. package/contracts/interfaces/IPolicyPool.sol +19 -1
  50. package/contracts/interfaces/IPolicyPoolComponent.sol +5 -1
  51. package/contracts/interfaces/IRiskModule.sol +67 -4
  52. package/contracts/mocks/InterfaceIdCalculator.sol +32 -0
  53. package/contracts/mocks/PolicyHolderMock.sol +21 -0
  54. package/contracts/mocks/PolicyPoolComponentMock.sol +10 -0
  55. package/contracts/mocks/PolicyPoolMock.sol +31 -0
  56. package/contracts/mocks/RiskModuleMock.sol +7 -1
  57. package/js/deploy.js +734 -0
  58. package/js/test-utils.js +184 -69
  59. package/package.json +4 -2
  60. package/scripts/change-pragma.sh +19 -0
  61. package/scripts/deploySmokeTest-fork.sh +53 -0
  62. package/scripts/deploySmokeTest.sh +51 -0
  63. package/scripts/storageLayout.js +33 -0
  64. package/scripts/utils.sh +65 -0
  65. package/contracts/WadRayMath.sol +0 -135
@@ -11,7 +11,13 @@ import {PolicyPoolComponent} from "./PolicyPoolComponent.sol";
11
11
 
12
12
  /**
13
13
  * @title Base contract for Ensuro cash reserves
14
- * @dev This contract implements the methods related with management of the reserves and payments
14
+ * @dev This contract implements the methods related with management of the reserves and payments. {EToken} and
15
+ * {PremiumsAccount} inherit from this contract.
16
+ *
17
+ * These contracts have an asset manager {IAssetManager} that's a strategy contract that runs in the same context
18
+ * (called with delegatecall) that apply some strategy to reinvest the assets managed by the contract to generate
19
+ * additional returns.
20
+ *
15
21
  * @custom:security-contact security@ensuro.co
16
22
  * @author Ensuro
17
23
  */
@@ -21,42 +27,116 @@ abstract contract Reserve is PolicyPoolComponent {
21
27
 
22
28
  /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
23
29
  // solhint-disable-next-line var-name-mixedcase
24
- uint256 public immutable NEGLIGIBLE_AMOUNT; // init as 10**(decimals/2) == 0.001 USD
30
+ uint256 internal immutable NEGLIGIBLE_AMOUNT; // init as 10**(decimals/2) == 0.001 USD
25
31
 
32
+ /**
33
+ * @dev Reserve constructor. Calculates NEGLIGIBLE_AMOUNT to avoid rounding errors.
34
+ *
35
+ * @param policyPool_ The {PolicyPool} where this reserve will be plugged
36
+ */
26
37
  /// @custom:oz-upgrades-unsafe-allow constructor
27
38
  constructor(IPolicyPool policyPool_) PolicyPoolComponent(policyPool_) {
28
39
  NEGLIGIBLE_AMOUNT = 10**(policyPool_.currency().decimals() / 2);
29
40
  }
30
41
 
42
+ /**
43
+ * @dev Initializes the Reserve (to be called by subclasses)
44
+ */
45
+ // solhint-disable-next-line func-name-mixedcase
46
+ function __Reserve_init() internal onlyInitializing {
47
+ __PolicyPoolComponent_init();
48
+ }
49
+
50
+ /**
51
+ * @dev Refills the reserve's balance, deinvesting from the asset manager to be able to make a payment
52
+ *
53
+ * @param amount The amount of the payment that needs to be made
54
+ * @return Returns the actual amount deinvested (how much the `currency().balanceof(this)` was increased). It might be
55
+ * more than `amount` because the asset manager might want to give more liquidity to the reserve to avoid further
56
+ * deinvestments. After the call, the `currency().balanceof(this)` should be greater than `amount` (unless unsolvency
57
+ * problem).
58
+ */
59
+ function _refillWallet(uint256 amount) internal returns (uint256) {
60
+ address am = address(assetManager());
61
+ if (am != address(0)) {
62
+ bytes memory result = am.functionDelegateCall(
63
+ abi.encodeWithSelector(IAssetManager.refillWallet.selector, amount),
64
+ "Error refilling wallet"
65
+ );
66
+ return abi.decode(result, (uint256));
67
+ }
68
+ return 0;
69
+ }
70
+
71
+ /**
72
+ * @dev Internal function that transfers money to a destination. It might need to call `_refillWallet` to deinvest
73
+ * some money to have enough liquidity for the payment.
74
+ *
75
+ * @param destination The destination of the transfer.
76
+ * @param amount The amount to be transferred.
77
+ */
31
78
  function _transferTo(address destination, uint256 amount) internal {
79
+ require(destination != address(0), "Reserve: transfer to the zero address");
32
80
  if (amount == 0) return;
33
81
  uint256 balance = currency().balanceOf(address(this));
34
82
  if (balance < amount) {
35
- address am = address(assetManager());
36
- if (am != address(0)) {
37
- am.functionDelegateCall(
38
- abi.encodeWithSelector(IAssetManager.refillWallet.selector, amount),
39
- "Error refilling wallet"
40
- );
83
+ balance += _refillWallet(amount);
84
+ if (amount > balance) {
85
+ if ((amount - balance) < NEGLIGIBLE_AMOUNT) {
86
+ amount = balance;
87
+ } // else - No need to do anything since safeTransfer will fail anyway
41
88
  }
42
- if ((amount - balance) < NEGLIGIBLE_AMOUNT) amount = balance;
43
89
  }
44
90
  currency().safeTransfer(destination, amount);
45
91
  }
46
92
 
93
+ /**
94
+ * @dev Returns the address of the asset manager for this reserve. The asset manager is the contract that manages the
95
+ * funds to generate additional yields. Can be `address(0)` if no asset manager has been set.
96
+ */
47
97
  function assetManager() public view virtual returns (IAssetManager);
48
98
 
99
+ /**
100
+ * @dev Internal function that needs to be implemented by child contracts because they might store the asset manager
101
+ * address in a different way. This function just stores the value, doesn't do any validation (validations are done on
102
+ * `setAssetManager`.
103
+ *
104
+ * @param newAM The address of the new asset manager for the reserve.
105
+ */
49
106
  function _setAssetManager(IAssetManager newAM) internal virtual;
50
107
 
108
+ /**
109
+ * @dev Internal function that needs to be implemented by child contracts to record the earnings (or losses if
110
+ * negative) generated by the asset management.
111
+ *
112
+ * @param earnings The amount of earnings (or losses if negative) generated since last time the earnings were
113
+ * recorded.
114
+ */
51
115
  function _assetEarnings(int256 earnings) internal virtual;
52
116
 
117
+ /**
118
+ * @dev Sets the asset manager for this reserve. If the reserve had previously an asset manager, it will deinvest all
119
+ * the funds, making all of the liquid in the reserve balance.
120
+ *
121
+ * Requirements:
122
+ * - The caller must have been granted of global or component roles GUARDIAN_ROLE or LEVEL1_ROLE.
123
+ *
124
+ * Events:
125
+ * - Emits ComponentChanged with action setAssetManager or setAssetManagerForced
126
+ *
127
+ * @param newAM The address of the new asset manager to assign to the reserve. If is `address(0)` it means the reserve
128
+ * will not have an asset manager. If not `address(0)` it MUST be a contract following the IAssetManager interface.
129
+ * @param force When a previous asset manager exists, before setting the new one, the funds are deinvested. When
130
+ * `force` is true, an error in the deinvestAll() operation is ignored. When `force` is false, if `deinvestAll()`
131
+ * fails, it reverts.
132
+ */
53
133
  function setAssetManager(IAssetManager newAM, bool force)
54
134
  external
55
135
  onlyGlobalOrComponentRole2(GUARDIAN_ROLE, LEVEL1_ROLE)
56
136
  {
57
137
  require(
58
- address(newAM) == address(0) || address(newAM).isContract(),
59
- "The assetManager is not a contract!"
138
+ address(newAM) == address(0) || newAM.supportsInterface(type(IAssetManager).interfaceId),
139
+ "Reserve: asset manager doesn't implements the required interface"
60
140
  );
61
141
  address am = address(assetManager());
62
142
  IAccessManager.GovernanceActions action = IAccessManager.GovernanceActions.setAssetManager;
@@ -87,32 +167,63 @@ abstract contract Reserve is PolicyPoolComponent {
87
167
  _componentChanged(action, address(newAM));
88
168
  }
89
169
 
170
+ /**
171
+ * @dev Calls {IAssetManager-rebalance} of the assigned asset manager (fails if no asset manager). This operation is
172
+ * intended to give the opportunity to rebalance the liquid and invested for better returns and/or gas optimization.
173
+ *
174
+ * - Emits {IAssetManager-MoneyInvested} or {IAssetManager-MoneyDeinvested}
175
+ */
90
176
  function rebalance() public whenNotPaused {
91
- address am = address(assetManager());
92
- require(am != address(0), "No asset manager");
93
- am.functionDelegateCall(abi.encodeWithSelector(IAssetManager.rebalance.selector));
177
+ address(assetManager()).functionDelegateCall(
178
+ abi.encodeWithSelector(IAssetManager.rebalance.selector)
179
+ );
94
180
  }
95
181
 
182
+ /**
183
+ * @dev Calls {IAssetManager-recordEarnings} of the assigned asset manager (fails if no asset manager). The asset
184
+ * manager will return the earnings since last time the earnings where recorded. It then calls `_assetEarnings` to
185
+ * reflect the earnings in the way defined for each reserve.
186
+ *
187
+ * - Emits {IAssetManager-EarningsRecorded}
188
+ */
96
189
  function recordEarnings() public whenNotPaused {
97
- address am = address(assetManager());
98
- require(am != address(0), "No asset manager");
99
- bytes memory result = am.functionDelegateCall(
190
+ bytes memory result = address(assetManager()).functionDelegateCall(
100
191
  abi.encodeWithSelector(IAssetManager.recordEarnings.selector)
101
192
  );
102
193
  _assetEarnings(abi.decode(result, (int256)));
103
194
  }
104
195
 
196
+ /**
197
+ * @dev Function that calls both `recordEarnings()` and `rebalance()` (in that order). Usually scheduled to run once a
198
+ * day by a keeper or crontask.
199
+ */
105
200
  function checkpoint() external whenNotPaused {
106
201
  recordEarnings();
107
202
  rebalance();
108
203
  }
109
204
 
205
+ /**
206
+ * @dev This function allows to call custom functions of the asset manager (for example for setting parameters).
207
+ * This functions will be called with `delegatecall`, in the context of the reserve.
208
+ *
209
+ * Requirements:
210
+ * - The caller must have been granted of global or component roles LEVEL2_ROLE.
211
+ *
212
+ * @param functionCall Abi encoded function call to make.
213
+ * @return Returns the return value of the function called, to be decoded by the receiver.
214
+ */
110
215
  function forwardToAssetManager(bytes memory functionCall)
111
216
  external
112
217
  onlyGlobalOrComponentRole(LEVEL2_ROLE)
113
218
  returns (bytes memory)
114
219
  {
115
- address am = address(assetManager());
116
- return am.functionDelegateCall(functionCall);
220
+ return address(assetManager()).functionDelegateCall(functionCall);
117
221
  }
222
+
223
+ /**
224
+ * @dev This empty reserved space is put in place to allow future versions to add new
225
+ * variables without shifting down storage in the inheritance chain.
226
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
227
+ */
228
+ uint256[50] private __gap;
118
229
  }
@@ -1,7 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {WadRayMath} from "./WadRayMath.sol";
4
+ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
5
+ import {WadRayMath} from "./dependencies/WadRayMath.sol";
5
6
  import {IPolicyPool} from "./interfaces/IPolicyPool.sol";
6
7
  import {PolicyPoolComponent} from "./PolicyPoolComponent.sol";
7
8
  import {IRiskModule} from "./interfaces/IRiskModule.sol";
@@ -18,8 +19,15 @@ import {Policy} from "./Policy.sol";
18
19
  abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
19
20
  using Policy for Policy.PolicyData;
20
21
  using WadRayMath for uint256;
22
+ using SafeCast for uint256;
21
23
 
22
24
  uint256 internal constant SECONDS_IN_YEAR_WAD = 31536000e18; /* 365 * 24 * 3600 * 10e18 */
25
+ uint16 internal constant HOURS_PER_YEAR = 8760; /* 24 * 365 */
26
+
27
+ uint256 internal constant FOUR_DECIMAL_TO_WAD = 1e14;
28
+ uint16 internal constant HUNDRED_PERCENT = 1e4;
29
+ uint16 internal constant MIN_MOC = 5e3;
30
+ uint16 internal constant MAX_MOC = 4e4;
23
31
 
24
32
  // For parameters that can be changed by the risk module provider
25
33
  bytes32 public constant RM_PROVIDER_ROLE = keccak256("RM_PROVIDER_ROLE");
@@ -78,7 +86,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
78
86
  uint256 maxPayoutPerPolicy_,
79
87
  uint256 exposureLimit_,
80
88
  address wallet_
81
- ) internal initializer {
89
+ ) internal onlyInitializing {
82
90
  __PolicyPoolComponent_init();
83
91
  __RiskModule_init_unchained(
84
92
  name_,
@@ -100,10 +108,10 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
100
108
  uint256 maxPayoutPerPolicy_,
101
109
  uint256 exposureLimit_,
102
110
  address wallet_
103
- ) internal initializer {
111
+ ) internal onlyInitializing {
104
112
  _name = name_;
105
113
  _params = PackedParams({
106
- moc: 1e4,
114
+ moc: HUNDRED_PERCENT,
107
115
  jrCollRatio: 0,
108
116
  collRatio: _wadTo4(collRatio_),
109
117
  ensuroPpFee: _wadTo4(ensuroPpFee_),
@@ -112,23 +120,42 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
112
120
  srRoc: _wadTo4(srRoc_),
113
121
  maxPayoutPerPolicy: _amountToX(2, maxPayoutPerPolicy_),
114
122
  exposureLimit: _amountToX(0, exposureLimit_),
115
- maxDuration: 24 * 365 // default = 1 year
123
+ maxDuration: HOURS_PER_YEAR
116
124
  });
117
125
  _activeExposure = 0;
118
126
  _wallet = wallet_;
119
127
  _validateParameters();
120
128
  }
121
129
 
130
+ function _upgradeValidations(address newImpl) internal view virtual override {
131
+ super._upgradeValidations(newImpl);
132
+ IRiskModule newRM = IRiskModule(newImpl);
133
+ require(
134
+ newRM.premiumsAccount() == _premiumsAccount,
135
+ "Can't upgrade changing the PremiumsAccount"
136
+ );
137
+ }
138
+
139
+ /**
140
+ * @dev See {IERC165-supportsInterface}.
141
+ */
142
+ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
143
+ return super.supportsInterface(interfaceId) || interfaceId == type(IRiskModule).interfaceId;
144
+ }
145
+
122
146
  // runs validation on RiskModule parameters
123
147
  function _validateParameters() internal view override {
124
- require(_params.jrCollRatio <= 1e4, "Validation: jrCollRatio must be <=1");
125
- require(_params.collRatio <= 1e4 && _params.collRatio > 0, "Validation: collRatio must be <=1");
148
+ require(_params.jrCollRatio <= HUNDRED_PERCENT, "Validation: jrCollRatio must be <=1");
149
+ require(
150
+ _params.collRatio <= HUNDRED_PERCENT && _params.collRatio > 0,
151
+ "Validation: collRatio must be <=1"
152
+ );
126
153
  require(_params.collRatio >= _params.jrCollRatio, "Validation: collRatio >= jrCollRatio");
127
- require(_params.moc <= 4e4 && _params.moc >= 5e3, "Validation: moc must be [0.5, 4]");
128
- require(_params.ensuroPpFee <= 1e4, "Validation: ensuroPpFee must be <= 1");
129
- require(_params.ensuroCocFee <= 1e4, "Validation: ensuroCocFee must be <= 1");
130
- require(_params.srRoc <= 1e4, "Validation: srRoc must be <= 1 (100%)");
131
- require(_params.jrRoc <= 1e4, "Validation: jrRoc must be <= 1 (100%)");
154
+ require(_params.moc <= MAX_MOC && _params.moc >= MIN_MOC, "Validation: moc must be [0.5, 4]");
155
+ require(_params.ensuroPpFee <= HUNDRED_PERCENT, "Validation: ensuroPpFee must be <= 1");
156
+ require(_params.ensuroCocFee <= HUNDRED_PERCENT, "Validation: ensuroCocFee must be <= 1");
157
+ require(_params.srRoc <= HUNDRED_PERCENT, "Validation: srRoc must be <= 1 (100%)");
158
+ require(_params.jrRoc <= HUNDRED_PERCENT, "Validation: jrRoc must be <= 1 (100%)");
132
159
  // _maxPayoutPerPolicy no limits
133
160
  require(
134
161
  exposureLimit() >= _activeExposure,
@@ -148,12 +175,12 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
148
175
  // solhint-disable-next-line func-name-mixedcase
149
176
  function _4toWad(uint16 value) internal pure returns (uint256) {
150
177
  // 4 decimals to Wad (18 decimals)
151
- return uint256(value) * 1e14;
178
+ return uint256(value) * FOUR_DECIMAL_TO_WAD;
152
179
  }
153
180
 
154
181
  function _wadTo4(uint256 value) internal pure returns (uint16) {
155
182
  // Wad to 4 decimals
156
- return uint16(value / 1e14);
183
+ return (value / FOUR_DECIMAL_TO_WAD).toUint16();
157
184
  }
158
185
 
159
186
  // solhint-disable-next-line func-name-mixedcase
@@ -164,7 +191,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
164
191
 
165
192
  function _amountToX(uint8 decimals, uint256 value) internal view returns (uint32) {
166
193
  // Wad to X decimals
167
- return uint32(value / 10**(currency().decimals() - decimals));
194
+ return (value / 10**(currency().decimals() - decimals)).toUint32();
168
195
  }
169
196
 
170
197
  function maxPayoutPerPolicy() public view override returns (uint256) {
@@ -235,9 +262,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
235
262
  _params.exposureLimit = _amountToX(0, newValue);
236
263
  } else if (param == Parameter.maxDuration) {
237
264
  require(!tweak, "Tweak exceeded");
238
- _params.maxDuration = uint16(newValue);
239
- } else {
240
- revert("Invalid param!");
265
+ _params.maxDuration = newValue.toUint16();
241
266
  }
242
267
  _parameterChanged(
243
268
  IAccessManager.GovernanceActions(
@@ -262,6 +287,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
262
287
  }
263
288
 
264
289
  function setWallet(address wallet_) external onlyComponentRole(RM_PROVIDER_ROLE) {
290
+ require(wallet_ != address(0), "RiskModule: wallet cannot be the zero address");
265
291
  _wallet = wallet_;
266
292
  _parameterChanged(IAccessManager.GovernanceActions.setWallet, uint256(uint160(wallet_)), false);
267
293
  }
@@ -329,7 +355,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
329
355
  "You must allow ENSURO to transfer the premium"
330
356
  );
331
357
  require(
332
- payer == msg.sender || _policyPool.currency().allowance(payer, msg.sender) >= premium,
358
+ payer == _msgSender() || _policyPool.currency().allowance(payer, _msgSender()) >= premium,
333
359
  "Payer must allow caller to transfer the premium"
334
360
  );
335
361
  require(payout <= maxPayoutPerPolicy(), "RiskModule: Payout is more than maximum per policy");
@@ -342,7 +368,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
342
368
  expiration
343
369
  );
344
370
  _activeExposure += policy.payout;
345
- require(_activeExposure <= exposureLimit(), "RiskModule: SCR limit exceeded");
371
+ require(_activeExposure <= exposureLimit(), "RiskModule: Exposure limit exceeded");
346
372
  uint256 policyId = _policyPool.newPolicy(policy, payer, onBehalfOf, internalId);
347
373
  policy.id = policyId;
348
374
  return policy;
@@ -358,4 +384,11 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
358
384
  function premiumsAccount() external view override returns (IPremiumsAccount) {
359
385
  return _premiumsAccount;
360
386
  }
387
+
388
+ /**
389
+ * @dev This empty reserved space is put in place to allow future versions to add new
390
+ * variables without shifting down storage in the inheritance chain.
391
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
392
+ */
393
+ uint256[46] private __gap;
361
394
  }
@@ -0,0 +1,310 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
5
+ import {IPolicyPool} from "./interfaces/IPolicyPool.sol";
6
+ import {IPremiumsAccount} from "./interfaces/IPremiumsAccount.sol";
7
+ import {RiskModule} from "./RiskModule.sol";
8
+ import {Policy} from "./Policy.sol";
9
+
10
+ /**
11
+ * @title 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
+ * @custom:security-contact security@ensuro.co
15
+ * @author Ensuro
16
+ */
17
+ contract SignedQuoteRiskModule is RiskModule {
18
+ bytes32 public constant POLICY_CREATOR_ROLE = keccak256("POLICY_CREATOR_ROLE");
19
+ bytes32 public constant PRICER_ROLE = keccak256("PRICER_ROLE");
20
+ bytes32 public constant RESOLVER_ROLE = keccak256("RESOLVER_ROLE");
21
+
22
+ /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
23
+ bool internal immutable _creationIsOpen;
24
+
25
+ /**
26
+ * @dev Event emitted every time a new policy is created. It allows to link the policyData with a particular policy
27
+ *
28
+ * @param policyId The id of the policy
29
+ * @param policyData The value sent in `policyData` parameter that's the hash of the off-chain stored data.
30
+ */
31
+ event NewSignedPolicy(uint256 indexed policyId, bytes32 policyData);
32
+
33
+ /// @custom:oz-upgrades-unsafe-allow constructor
34
+ constructor(
35
+ IPolicyPool policyPool_,
36
+ IPremiumsAccount premiumsAccount_,
37
+ bool creationIsOpen_
38
+ ) RiskModule(policyPool_, premiumsAccount_) {
39
+ _creationIsOpen = creationIsOpen_;
40
+ }
41
+
42
+ /**
43
+ * @dev Initializes the RiskModule
44
+ * @param name_ Name of the Risk Module
45
+ * @param collRatio_ Collateralization ratio to compute solvency requirement as % of payout (in ray)
46
+ * @param ensuroPpFee_ % of pure premium that will go for Ensuro treasury (in ray)
47
+ * @param srRoc_ return on capital paid to Senior LPs (annualized percentage - in ray)
48
+ * @param maxPayoutPerPolicy_ Maximum payout per policy (in wad)
49
+ * @param exposureLimit_ Max exposure (sum of payouts) to be allocated to this module (in wad)
50
+ * @param wallet_ Address of the RiskModule provider
51
+ */
52
+ function initialize(
53
+ string memory name_,
54
+ uint256 collRatio_,
55
+ uint256 ensuroPpFee_,
56
+ uint256 srRoc_,
57
+ uint256 maxPayoutPerPolicy_,
58
+ uint256 exposureLimit_,
59
+ address wallet_
60
+ ) public initializer {
61
+ __RiskModule_init(
62
+ name_,
63
+ collRatio_,
64
+ ensuroPpFee_,
65
+ srRoc_,
66
+ maxPayoutPerPolicy_,
67
+ exposureLimit_,
68
+ wallet_
69
+ );
70
+ }
71
+
72
+ function _newPolicySigned(
73
+ uint256 payout,
74
+ uint256 premium,
75
+ uint256 lossProb,
76
+ uint40 expiration,
77
+ bytes32 policyData,
78
+ bytes32 quoteSignatureR,
79
+ bytes32 quoteSignatureVS,
80
+ uint40 quoteValidUntil,
81
+ address payer,
82
+ address onBehalfOf
83
+ ) internal returns (Policy.PolicyData memory createdPolicy) {
84
+ if (!_creationIsOpen)
85
+ _policyPool.access().checkComponentRole(
86
+ address(this),
87
+ POLICY_CREATOR_ROLE,
88
+ _msgSender(),
89
+ false
90
+ );
91
+ require(quoteValidUntil >= block.timestamp, "Quote expired");
92
+
93
+ /**
94
+ * Checks the quote has been signed by an authorized user
95
+ * The "quote" is computed as hash of the following fields (encodePacked):
96
+ * - address(this): the address of this RiskModule
97
+ * - payout, premium, lossProb, expiration: the base parameters of the policy
98
+ * - policyData: a hash of the private details of the policy. The calculation should include some
99
+ * unique id (quoteId), so each policyData identifies a policy.
100
+ * - quoteValidUntil: the maximum validity of the quote
101
+ */
102
+ bytes32 quoteHash = ECDSA.toEthSignedMessageHash(
103
+ abi.encodePacked(
104
+ address(this),
105
+ payout,
106
+ premium,
107
+ lossProb,
108
+ expiration,
109
+ policyData,
110
+ quoteValidUntil
111
+ )
112
+ );
113
+ address signer = ECDSA.recover(quoteHash, quoteSignatureR, quoteSignatureVS);
114
+ _policyPool.access().checkComponentRole(address(this), PRICER_ROLE, signer, false);
115
+ uint96 internalId = uint96(uint256(policyData) % 2**96);
116
+
117
+ createdPolicy = _newPolicy(
118
+ payout,
119
+ premium,
120
+ lossProb,
121
+ expiration,
122
+ payer,
123
+ onBehalfOf,
124
+ internalId
125
+ );
126
+ emit NewSignedPolicy(createdPolicy.id, policyData);
127
+ return createdPolicy;
128
+ }
129
+
130
+ /**
131
+ * @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.
132
+ *
133
+ * Requirements:
134
+ * - The caller approved the spending of the premium to the PolicyPool
135
+ * - The quote has been signed by an address with the component role PRICER_ROLE
136
+ *
137
+ * Emits:
138
+ * - {PolicyPool.NewPolicy}
139
+ * - {NewSignedPolicy}
140
+ *
141
+ * @param payout The exposure (maximum payout) of the policy
142
+ * @param premium The premium that will be paid by the payer
143
+ * @param lossProb The probability of having to pay the maximum payout (wad)
144
+ * @param expiration The expiration of the policy (timestamp)
145
+ * @param onBehalfOf The policy holder
146
+ * @param policyData A hash of the private details of the policy. The last 96 bits will be used as internalId
147
+ * @param quoteSignatureR The signature of the quote. R component (EIP-2098 signature)
148
+ * @param quoteSignatureVS The signature of the quote. VS component (EIP-2098 signature)
149
+ * @param quoteValidUntil The expiration of the quote
150
+ * @return createdPolicy Returns the created policy
151
+ */
152
+ function newPolicyFull(
153
+ uint256 payout,
154
+ uint256 premium,
155
+ uint256 lossProb,
156
+ uint40 expiration,
157
+ address onBehalfOf,
158
+ bytes32 policyData,
159
+ bytes32 quoteSignatureR,
160
+ bytes32 quoteSignatureVS,
161
+ uint40 quoteValidUntil
162
+ ) external returns (Policy.PolicyData memory createdPolicy) {
163
+ return
164
+ _newPolicySigned(
165
+ payout,
166
+ premium,
167
+ lossProb,
168
+ expiration,
169
+ policyData,
170
+ quoteSignatureR,
171
+ quoteSignatureVS,
172
+ quoteValidUntil,
173
+ _msgSender(),
174
+ onBehalfOf
175
+ );
176
+ }
177
+
178
+ /**
179
+ * @dev Creates a new Policy using a signed quote. The caller is the payer of the policy.
180
+ *
181
+ * Requirements:
182
+ * - The caller approved the spending of the premium to the PolicyPool
183
+ * - The quote has been signed by an address with the component role PRICER_ROLE
184
+ *
185
+ * Emits:
186
+ * - {PolicyPool.NewPolicy}
187
+ * - {NewSignedPolicy}
188
+ *
189
+ * @param payout The exposure (maximum payout) of the policy
190
+ * @param premium The premium that will be paid by the payer
191
+ * @param lossProb The probability of having to pay the maximum payout (wad)
192
+ * @param expiration The expiration of the policy (timestamp)
193
+ * @param onBehalfOf The policy holder
194
+ * @param policyData A hash of the private details of the policy. The last 96 bits will be used as internalId
195
+ * @param quoteSignatureR The signature of the quote. R component (EIP-2098 signature)
196
+ * @param quoteSignatureVS The signature of the quote. VS component (EIP-2098 signature)
197
+ * @param quoteValidUntil The expiration of the quote
198
+ * @return Returns the id of the created policy
199
+ */
200
+ function newPolicy(
201
+ uint256 payout,
202
+ uint256 premium,
203
+ uint256 lossProb,
204
+ uint40 expiration,
205
+ address onBehalfOf,
206
+ bytes32 policyData,
207
+ bytes32 quoteSignatureR,
208
+ bytes32 quoteSignatureVS,
209
+ uint40 quoteValidUntil
210
+ ) external returns (uint256) {
211
+ return
212
+ _newPolicySigned(
213
+ payout,
214
+ premium,
215
+ lossProb,
216
+ expiration,
217
+ policyData,
218
+ quoteSignatureR,
219
+ quoteSignatureVS,
220
+ quoteValidUntil,
221
+ _msgSender(),
222
+ onBehalfOf
223
+ ).id;
224
+ }
225
+
226
+ /**
227
+ * @dev Creates a new Policy using a signed quote. The payer is the policy holder
228
+ *
229
+ * Requirements:
230
+ * - currency().allowance(onBehalfOf, _msgSender()) > 0
231
+ * - The quote has been signed by an address with the component role PRICER_ROLE
232
+ *
233
+ * Emits:
234
+ * - {PolicyPool.NewPolicy}
235
+ * - {NewSignedPolicy}
236
+ *
237
+ * @param payout The exposure (maximum payout) of the policy
238
+ * @param premium The premium that will be paid by the payer
239
+ * @param lossProb The probability of having to pay the maximum payout (wad)
240
+ * @param expiration The expiration of the policy (timestamp)
241
+ * @param onBehalfOf The policy holder
242
+ * @param policyData A hash of the private details of the policy. The last 96 bits will be used as internalId
243
+ * @param quoteSignatureR The signature of the quote. R component (EIP-2098 signature)
244
+ * @param quoteSignatureVS The signature of the quote. VS component (EIP-2098 signature)
245
+ * @param quoteValidUntil The expiration of the quote
246
+ * @return Returns the id of the created policy
247
+ */
248
+ function newPolicyPaidByHolder(
249
+ uint256 payout,
250
+ uint256 premium,
251
+ uint256 lossProb,
252
+ uint40 expiration,
253
+ address onBehalfOf,
254
+ bytes32 policyData,
255
+ bytes32 quoteSignatureR,
256
+ bytes32 quoteSignatureVS,
257
+ uint40 quoteValidUntil
258
+ ) external returns (uint256) {
259
+ require(
260
+ onBehalfOf == _msgSender() || currency().allowance(onBehalfOf, _msgSender()) > 0,
261
+ "Sender is not authorized to create policies onBehalfOf"
262
+ );
263
+ /**
264
+ * The standard is the payer should be the _msgSender() but usually, in this type of module,
265
+ * the sender is an operative account managed by software, where the onBehalfOf is a more
266
+ * secure account (hardware wallet) that does the cash movements.
267
+ * This non standard behaviour allows for a more secure setup, where the sender never manages
268
+ * cash.
269
+ * We leverage the currency's allowance mechanism to allow the sender access to the payer's
270
+ * funds.
271
+ * Note that this allowance won't be spent, so anything above 0 is accepted.
272
+ */
273
+ return
274
+ _newPolicySigned(
275
+ payout,
276
+ premium,
277
+ lossProb,
278
+ expiration,
279
+ policyData,
280
+ quoteSignatureR,
281
+ quoteSignatureVS,
282
+ quoteValidUntil,
283
+ onBehalfOf,
284
+ onBehalfOf
285
+ ).id;
286
+ }
287
+
288
+ function resolvePolicy(Policy.PolicyData calldata policy, uint256 payout)
289
+ external
290
+ onlyComponentRole(RESOLVER_ROLE)
291
+ whenNotPaused
292
+ {
293
+ _policyPool.resolvePolicy(policy, payout);
294
+ }
295
+
296
+ function resolvePolicyFullPayout(Policy.PolicyData calldata policy, bool customerWon)
297
+ external
298
+ onlyComponentRole(RESOLVER_ROLE)
299
+ whenNotPaused
300
+ {
301
+ _policyPool.resolvePolicyFullPayout(policy, customerWon);
302
+ }
303
+
304
+ /**
305
+ * @dev This empty reserved space is put in place to allow future versions to add new
306
+ * variables without shifting down storage in the inheritance chain.
307
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
308
+ */
309
+ uint256[50] private __gap;
310
+ }