@ensuro/core 2.0.0-beta1 → 2.0.0-beta10

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 (79) hide show
  1. package/README.md +23 -14
  2. package/build/contracts/AccessManager.sol/AccessManager.json +559 -0
  3. package/build/contracts/ERC4626AssetManager.sol/ERC4626AssetManager.json +219 -0
  4. package/build/contracts/EToken.sol/EToken.json +134 -52
  5. package/build/contracts/LPManualWhitelist.sol/LPManualWhitelist.json +4 -4
  6. package/build/contracts/LiquidityThresholdAssetManager.sol/LiquidityThresholdAssetManager.json +203 -0
  7. package/build/contracts/Policy.sol/Policy.json +2 -2
  8. package/build/contracts/PolicyPool.sol/PolicyPool.json +437 -61
  9. package/build/contracts/PolicyPoolComponent.sol/PolicyPoolComponent.json +2 -2
  10. package/build/contracts/PremiumsAccount.sol/PremiumsAccount.json +123 -16
  11. package/build/contracts/Reserve.sol/Reserve.json +66 -8
  12. package/build/contracts/RiskModule.sol/RiskModule.json +2 -2
  13. package/build/contracts/SignedQuoteRiskModule.sol/SignedQuoteRiskModule.json +960 -0
  14. package/build/contracts/TimeScaled.sol/TimeScaled.json +3 -17
  15. package/build/contracts/TrustfulRiskModule.sol/TrustfulRiskModule.json +5 -5
  16. package/build/contracts/{WadRayMath.sol → dependencies/WadRayMath.sol}/WadRayMath.json +3 -3
  17. package/build/contracts/interfaces/{IPolicyPoolConfig.sol/IPolicyPoolConfig.json → IAccessManager.sol/IAccessManager.json} +77 -59
  18. package/build/contracts/interfaces/IAssetManager.sol/IAssetManager.json +109 -0
  19. package/build/contracts/interfaces/IEToken.sol/IEToken.json +13 -0
  20. package/build/contracts/interfaces/IPolicyPool.sol/IPolicyPool.json +40 -16
  21. package/build/contracts/interfaces/IPremiumsAccount.sol/IPremiumsAccount.json +13 -0
  22. package/build/contracts/mocks/FixedRateVault.sol/FixedRateVault.json +736 -0
  23. package/build/contracts/mocks/ForwardProxy.sol/ForwardProxy.json +2 -2
  24. package/build/contracts/mocks/PolicyHolderMock.sol/PolicyHolderMock.json +28 -2
  25. package/build/contracts/mocks/PolicyPoolComponentMock.sol/PolicyPoolComponentMock.json +2 -2
  26. package/build/contracts/mocks/PolicyPoolMock.sol/PolicyPoolMock.json +118 -19
  27. package/build/contracts/mocks/PolicyPoolMock.sol/PolicyPoolMockForward.json +6 -6
  28. package/build/contracts/mocks/RiskModuleMock.sol/RiskModuleMock.json +764 -0
  29. package/build/contracts/mocks/TestCurrency.sol/TestCurrency.json +2 -2
  30. package/build/contracts/mocks/TestNFT.sol/TestNFT.json +2 -2
  31. package/contracts/AccessManager.sol +103 -0
  32. package/contracts/ERC4626AssetManager.sol +52 -0
  33. package/contracts/EToken.sol +123 -87
  34. package/contracts/LPManualWhitelist.sol +1 -1
  35. package/contracts/LiquidityThresholdAssetManager.sol +160 -0
  36. package/contracts/Policy.sol +1 -1
  37. package/contracts/PolicyPool.sol +333 -115
  38. package/contracts/PolicyPoolComponent.sol +36 -41
  39. package/contracts/PremiumsAccount.sol +285 -79
  40. package/contracts/Reserve.sol +183 -4
  41. package/contracts/RiskModule.sol +26 -19
  42. package/contracts/SignedQuoteRiskModule.sol +241 -0
  43. package/contracts/TimeScaled.sol +19 -15
  44. package/contracts/TrustfulRiskModule.sol +62 -2
  45. package/contracts/dependencies/WadRayMath.sol +126 -0
  46. package/contracts/interfaces/IAccessManager.sol +148 -0
  47. package/contracts/interfaces/IAssetManager.sol +84 -0
  48. package/contracts/interfaces/IEToken.sol +25 -2
  49. package/contracts/interfaces/ILPWhitelist.sol +17 -0
  50. package/contracts/interfaces/IPolicyPool.sol +20 -13
  51. package/contracts/interfaces/IPolicyPoolComponent.sol +3 -0
  52. package/contracts/interfaces/IPremiumsAccount.sol +5 -0
  53. package/contracts/interfaces/IRiskModule.sol +67 -4
  54. package/contracts/mocks/FixedRateVault.sol +85 -0
  55. package/contracts/mocks/PolicyHolderMock.sol +21 -0
  56. package/contracts/mocks/PolicyPoolMock.sol +41 -15
  57. package/contracts/mocks/RiskModuleMock.sol +75 -0
  58. package/js/test-utils.js +227 -149
  59. package/package.json +1 -1
  60. package/build/contracts/FlightDelayRiskModule.sol/FlightDelayRiskModule.json +0 -890
  61. package/build/contracts/PolicyNFT.sol/PolicyNFT.json +0 -583
  62. package/build/contracts/PolicyPoolConfig.sol/PolicyPoolConfig.json +0 -691
  63. package/build/contracts/dependencies/ChainlinkClientUpgradeable.sol/ChainlinkClientUpgradeable.json +0 -63
  64. package/build/contracts/interfaces/IPolicyNFT.sol/IPolicyNFT.json +0 -321
  65. package/build/contracts/mocks/LinkTokenMock.sol/LinkTokenMock.json +0 -370
  66. package/build/contracts/mocks/TestCurrencyUUPS.sol/TestCurrencyUUPS.json +0 -353
  67. package/build/contracts/upgraded/IPolicyNFTv1.sol/IPolicyNFTv1.json +0 -322
  68. package/build/contracts/upgraded/PolicyNFTv1.sol/PolicyNFTv1.json +0 -584
  69. package/build/contracts/upgraded/PolicyNFTv1.sol/PolicyNFTv1_Upgrade.json +0 -615
  70. package/contracts/FlightDelayRiskModule.sol +0 -217
  71. package/contracts/PolicyNFT.sol +0 -96
  72. package/contracts/PolicyPoolConfig.sol +0 -204
  73. package/contracts/WadRayMath.sol +0 -135
  74. package/contracts/dependencies/ChainlinkClientUpgradeable.sol +0 -371
  75. package/contracts/interfaces/IPolicyNFT.sol +0 -15
  76. package/contracts/interfaces/IPolicyPoolConfig.sol +0 -96
  77. package/contracts/mocks/LinkTokenMock.sol +0 -30
  78. package/contracts/upgraded/IPolicyNFTv1.sol +0 -15
  79. package/contracts/upgraded/PolicyNFTv1.sol +0 -126
@@ -2,33 +2,212 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
5
+ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
5
6
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6
7
  import {IPolicyPool} from "./interfaces/IPolicyPool.sol";
8
+ import {IAssetManager} from "./interfaces/IAssetManager.sol";
9
+ import {IAccessManager} from "./interfaces/IAccessManager.sol";
7
10
  import {PolicyPoolComponent} from "./PolicyPoolComponent.sol";
8
11
 
9
12
  /**
10
13
  * @title Base contract for Ensuro cash reserves
11
- * @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
+ *
12
21
  * @custom:security-contact security@ensuro.co
13
22
  * @author Ensuro
14
23
  */
15
24
  abstract contract Reserve is PolicyPoolComponent {
16
25
  using SafeERC20 for IERC20Metadata;
26
+ using Address for address;
17
27
 
18
28
  /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
19
29
  // solhint-disable-next-line var-name-mixedcase
20
- 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
21
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
+ */
22
37
  /// @custom:oz-upgrades-unsafe-allow constructor
23
38
  constructor(IPolicyPool policyPool_) PolicyPoolComponent(policyPool_) {
24
39
  NEGLIGIBLE_AMOUNT = 10**(policyPool_.currency().decimals() / 2);
25
40
  }
26
41
 
42
+ /**
43
+ * @dev Refills the reserve's balance, deinvesting from the asset manager to be able to make a payment
44
+ *
45
+ * @param amount The amount of the payment that needs to be made
46
+ * @return Returns the actual amount deinvested (how much the `currency().balanceof(this)` was increased). It might be
47
+ * more than `amount` because the asset manager might want to give more liquidity to the reserve to avoid further
48
+ * deinvestments. After the call, the `currency().balanceof(this)` should be greater than `amount` (unless unsolvency
49
+ * problem).
50
+ */
51
+ function _refillWallet(uint256 amount) internal returns (uint256) {
52
+ address am = address(assetManager());
53
+ if (am != address(0)) {
54
+ bytes memory result = am.functionDelegateCall(
55
+ abi.encodeWithSelector(IAssetManager.refillWallet.selector, amount),
56
+ "Error refilling wallet"
57
+ );
58
+ return abi.decode(result, (uint256));
59
+ }
60
+ return 0;
61
+ }
62
+
63
+ /**
64
+ * @dev Internal function that transfers money to a destination. It might need to call `_refillWallet` to deinvest
65
+ * some money to have enought liquidity for the payment.
66
+ *
67
+ * @param destination The destination of the transfer.
68
+ * @param amount The amount to be transferred.
69
+ */
27
70
  function _transferTo(address destination, uint256 amount) internal {
28
71
  if (amount == 0) return;
29
72
  uint256 balance = currency().balanceOf(address(this));
30
- if (balance < amount && (amount - balance) < NEGLIGIBLE_AMOUNT) amount = balance;
31
- // TODO: asset management
73
+ if (balance < amount) {
74
+ balance += _refillWallet(amount);
75
+ if (amount > balance) {
76
+ if ((amount - balance) < NEGLIGIBLE_AMOUNT) {
77
+ amount = balance;
78
+ } // else - No need to do anything since safeTransfer will fail anyway
79
+ }
80
+ }
32
81
  currency().safeTransfer(destination, amount);
33
82
  }
83
+
84
+ /**
85
+ * @dev Returns the address of the asset manager for this reserve. The asset manager is the contract that manages the
86
+ * funds to generate additional yields. Can be `address(0)` if no asset manager has been set.
87
+ */
88
+ function assetManager() public view virtual returns (IAssetManager);
89
+
90
+ /**
91
+ * @dev Internal function that needs to be implemented by child contracts because they might store the asset manager
92
+ * address in a different way. This function just stores the value, doesn't do any validation (validations are done on
93
+ * `setAssetManager`.
94
+ *
95
+ * @param newAM The address of the new asset manager for the reserve.
96
+ */
97
+ function _setAssetManager(IAssetManager newAM) internal virtual;
98
+
99
+ /**
100
+ * @dev Internal function that needs to be implemented by child contracts to record the earnings (or losses if
101
+ * negative) generated by the asset management.
102
+ *
103
+ * @param earnings The amount of earnings (or losses if negative) generated since last time the earnings were
104
+ * recorded.
105
+ */
106
+ function _assetEarnings(int256 earnings) internal virtual;
107
+
108
+ /**
109
+ * @dev Sets the asset manager for this reserve. If the reserve had previously an asset manager, it will deinvest all
110
+ * the funds, making all of the liquid in the reserve balance.
111
+ *
112
+ * Requirements:
113
+ * - The caller must have been granted of global or component roles GUARDIAN_ROLE or LEVEL1_ROLE.
114
+ *
115
+ * Events:
116
+ * - Emits ComponentChanged with action setAssetManager or setAssetManagerForced
117
+ *
118
+ * @param newAM The address of the new asset manager to assign to the reserve. If is `address(0)` it means the reserve
119
+ * will not have an asset manager. If not `address(0)` it MUST be a contract following the IAssetManager interface.
120
+ * @param force When a previous asset manager exists, before setting the new one, the funds are deinvested. When
121
+ * `force` is true, an error in the deinvestAll() operation is ignored. When `force` is false, if `deinvestAll()`
122
+ * fails, it reverts.
123
+ */
124
+ function setAssetManager(IAssetManager newAM, bool force)
125
+ external
126
+ onlyGlobalOrComponentRole2(GUARDIAN_ROLE, LEVEL1_ROLE)
127
+ {
128
+ require(
129
+ address(newAM) == address(0) || address(newAM).isContract(),
130
+ "The assetManager is not a contract!"
131
+ );
132
+ address am = address(assetManager());
133
+ IAccessManager.GovernanceActions action = IAccessManager.GovernanceActions.setAssetManager;
134
+ if (am != address(0)) {
135
+ if (force) {
136
+ // Ignores success or not
137
+ // solhint-disable-next-line avoid-low-level-calls
138
+ (bool success, bytes memory result) = am.delegatecall(
139
+ abi.encodeWithSelector(IAssetManager.deinvestAll.selector)
140
+ );
141
+ if (!success) {
142
+ action = IAccessManager.GovernanceActions.setAssetManagerForced;
143
+ } else {
144
+ _assetEarnings(abi.decode(result, (int256)));
145
+ }
146
+ } else {
147
+ bytes memory result = am.functionDelegateCall(
148
+ abi.encodeWithSelector(IAssetManager.deinvestAll.selector)
149
+ );
150
+ _assetEarnings(abi.decode(result, (int256)));
151
+ }
152
+ }
153
+ _setAssetManager(newAM);
154
+ am = address(assetManager());
155
+ if (am != address(0)) {
156
+ am.functionDelegateCall(abi.encodeWithSelector(IAssetManager.connect.selector));
157
+ }
158
+ _componentChanged(action, address(newAM));
159
+ }
160
+
161
+ /**
162
+ * @dev Calls {IAssetManager-rebalance} of the assigned asset manager (fails if no asset manager). This operation is
163
+ * intended to give the opportunity to rebalance the liquid and invested for better returns and/or gas optimization.
164
+ *
165
+ * - Emits {IAssetManager-MoneyInvested} or {IAssetManager-MoneyDeinvested}
166
+ */
167
+ function rebalance() public whenNotPaused {
168
+ address(assetManager()).functionDelegateCall(
169
+ abi.encodeWithSelector(IAssetManager.rebalance.selector)
170
+ );
171
+ }
172
+
173
+ /**
174
+ * @dev Calls {IAssetManager-recordEarnings} of the assigned asset manager (fails if no asset manager). The asset
175
+ * manager will return the earnings since last time the earnings where recorded. It then calls `_assetEarnings` to
176
+ * reflect the earnings in the way defined for each reserve.
177
+ *
178
+ * - Emits {IAssetManager-EarningsRecorded}
179
+ */
180
+ function recordEarnings() public whenNotPaused {
181
+ bytes memory result = address(assetManager()).functionDelegateCall(
182
+ abi.encodeWithSelector(IAssetManager.recordEarnings.selector)
183
+ );
184
+ _assetEarnings(abi.decode(result, (int256)));
185
+ }
186
+
187
+ /**
188
+ * @dev Function that calls both `recordEarnings()` and `rebalance()` (in that order). Usually scheduled to run once a
189
+ * day by a keeper or crontask.
190
+ */
191
+ function checkpoint() external whenNotPaused {
192
+ recordEarnings();
193
+ rebalance();
194
+ }
195
+
196
+ /**
197
+ * @dev This function allows to call custom functions of the asset manager (for example for setting parameters).
198
+ * This functions will be called with `delegatecall`, in the context of the reserve.
199
+ *
200
+ * Requirements:
201
+ * - The caller must have been granted of global or component roles LEVEL2_ROLE.
202
+ *
203
+ * @param functionCall Abi encoded function call to make.
204
+ * @return Returns the return value of the function called, to be decoded by the receiver.
205
+ */
206
+ function forwardToAssetManager(bytes memory functionCall)
207
+ external
208
+ onlyGlobalOrComponentRole(LEVEL2_ROLE)
209
+ returns (bytes memory)
210
+ {
211
+ return address(assetManager()).functionDelegateCall(functionCall);
212
+ }
34
213
  }
@@ -1,12 +1,13 @@
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";
8
9
  import {IPremiumsAccount} from "./interfaces/IPremiumsAccount.sol";
9
- import {IPolicyPoolConfig} from "./interfaces/IPolicyPoolConfig.sol";
10
+ import {IAccessManager} from "./interfaces/IAccessManager.sol";
10
11
  import {Policy} from "./Policy.sol";
11
12
 
12
13
  /**
@@ -18,6 +19,7 @@ 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 */
23
25
 
@@ -153,7 +155,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
153
155
 
154
156
  function _wadTo4(uint256 value) internal pure returns (uint16) {
155
157
  // Wad to 4 decimals
156
- return uint16(value / 1e14);
158
+ return (value / 1e14).toUint16();
157
159
  }
158
160
 
159
161
  // solhint-disable-next-line func-name-mixedcase
@@ -164,7 +166,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
164
166
 
165
167
  function _amountToX(uint8 decimals, uint256 value) internal view returns (uint32) {
166
168
  // Wad to X decimals
167
- return uint32(value / 10**(currency().decimals() - decimals));
169
+ return (value / 10**(currency().decimals() - decimals)).toUint32();
168
170
  }
169
171
 
170
172
  function maxPayoutPerPolicy() public view override returns (uint256) {
@@ -189,7 +191,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
189
191
 
190
192
  function setParam(Parameter param, uint256 newValue)
191
193
  external
192
- onlyPoolRole3(LEVEL1_ROLE, LEVEL2_ROLE, LEVEL3_ROLE)
194
+ onlyGlobalOrComponentRole3(LEVEL1_ROLE, LEVEL2_ROLE, LEVEL3_ROLE)
193
195
  {
194
196
  bool tweak = !hasPoolRole(LEVEL2_ROLE) && !hasPoolRole(LEVEL1_ROLE);
195
197
  if (param == Parameter.moc) {
@@ -235,11 +237,11 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
235
237
  _params.exposureLimit = _amountToX(0, newValue);
236
238
  } else if (param == Parameter.maxDuration) {
237
239
  require(!tweak, "Tweak exceeded");
238
- _params.maxDuration = uint16(newValue);
240
+ _params.maxDuration = newValue.toUint16();
239
241
  }
240
242
  _parameterChanged(
241
- IPolicyPoolConfig.GovernanceActions(
242
- uint256(IPolicyPoolConfig.GovernanceActions.setMoc) + uint256(param)
243
+ IAccessManager.GovernanceActions(
244
+ uint256(IAccessManager.GovernanceActions.setMoc) + uint256(param)
243
245
  ),
244
246
  newValue,
245
247
  tweak
@@ -261,11 +263,7 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
261
263
 
262
264
  function setWallet(address wallet_) external onlyComponentRole(RM_PROVIDER_ROLE) {
263
265
  _wallet = wallet_;
264
- _parameterChanged(
265
- IPolicyPoolConfig.GovernanceActions.setWallet,
266
- uint256(uint160(wallet_)),
267
- false
268
- );
266
+ _parameterChanged(IAccessManager.GovernanceActions.setWallet, uint256(uint160(wallet_)), false);
269
267
  }
270
268
 
271
269
  function getMinimumPremium(
@@ -304,8 +302,9 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
304
302
  * @param payout The exposure (maximum payout) of the policy
305
303
  * @param premium The premium that will be paid by the policyHolder
306
304
  * @param lossProb The probability of having to pay the maximum payout (wad)
305
+ * @param payer The account that pays for the premium
307
306
  * @param expiration The expiration of the policy (timestamp)
308
- * @param customer The policy holder
307
+ * @param onBehalfOf The policy holder
309
308
  * @param internalId An id that's unique within this module and it will be used to identify the policy
310
309
  */
311
310
  function _newPolicy(
@@ -313,18 +312,26 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
313
312
  uint256 premium,
314
313
  uint256 lossProb,
315
314
  uint40 expiration,
316
- address customer,
315
+ address payer,
316
+ address onBehalfOf,
317
317
  uint96 internalId
318
318
  ) internal whenNotPaused returns (Policy.PolicyData memory) {
319
+ if (premium == type(uint256).max) {
320
+ premium = getMinimumPremium(payout, lossProb, expiration);
321
+ }
319
322
  require(premium < payout, "Premium must be less than payout");
320
323
  uint40 now_ = uint40(block.timestamp);
321
324
  require(expiration > now_, "Expiration must be in the future");
322
325
  require(((expiration - now_) / 3600) < _params.maxDuration, "Policy exceeds max duration");
323
- require(customer != address(0), "Customer can't be zero address");
326
+ require(onBehalfOf != address(0), "Customer can't be zero address");
324
327
  require(
325
- _policyPool.currency().allowance(customer, address(_policyPool)) >= premium,
328
+ _policyPool.currency().allowance(payer, address(_policyPool)) >= premium,
326
329
  "You must allow ENSURO to transfer the premium"
327
330
  );
331
+ require(
332
+ payer == _msgSender() || _policyPool.currency().allowance(payer, _msgSender()) >= premium,
333
+ "Payer must allow caller to transfer the premium"
334
+ );
328
335
  require(payout <= maxPayoutPerPolicy(), "RiskModule: Payout is more than maximum per policy");
329
336
  Policy.PolicyData memory policy = Policy.initialize(
330
337
  this,
@@ -335,8 +342,8 @@ abstract contract RiskModule is IRiskModule, PolicyPoolComponent {
335
342
  expiration
336
343
  );
337
344
  _activeExposure += policy.payout;
338
- require(_activeExposure <= exposureLimit(), "RiskModule: SCR limit exceeded");
339
- uint256 policyId = _policyPool.newPolicy(policy, customer, internalId);
345
+ require(_activeExposure <= exposureLimit(), "RiskModule: Exposure limit exceeded");
346
+ uint256 policyId = _policyPool.newPolicy(policy, payer, onBehalfOf, internalId);
340
347
  policy.id = policyId;
341
348
  return policy;
342
349
  }
@@ -0,0 +1,241 @@
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
+ event NewSignedPolicy(uint256 indexed policyId, bytes32 policyData);
26
+
27
+ /// @custom:oz-upgrades-unsafe-allow constructor
28
+ constructor(
29
+ IPolicyPool policyPool_,
30
+ IPremiumsAccount premiumsAccount_,
31
+ bool creationIsOpen_
32
+ ) RiskModule(policyPool_, premiumsAccount_) {
33
+ _creationIsOpen = creationIsOpen_;
34
+ }
35
+
36
+ /**
37
+ * @dev Initializes the RiskModule
38
+ * @param name_ Name of the Risk Module
39
+ * @param collRatio_ Collateralization ratio to compute solvency requirement as % of payout (in ray)
40
+ * @param ensuroPpFee_ % of pure premium that will go for Ensuro treasury (in ray)
41
+ * @param srRoc_ return on capital paid to Senior LPs (annualized percentage - in ray)
42
+ * @param maxPayoutPerPolicy_ Maximum payout per policy (in wad)
43
+ * @param exposureLimit_ Max exposure (sum of payouts) to be allocated to this module (in wad)
44
+ * @param wallet_ Address of the RiskModule provider
45
+ */
46
+ function initialize(
47
+ string memory name_,
48
+ uint256 collRatio_,
49
+ uint256 ensuroPpFee_,
50
+ uint256 srRoc_,
51
+ uint256 maxPayoutPerPolicy_,
52
+ uint256 exposureLimit_,
53
+ address wallet_
54
+ ) public initializer {
55
+ __RiskModule_init(
56
+ name_,
57
+ collRatio_,
58
+ ensuroPpFee_,
59
+ srRoc_,
60
+ maxPayoutPerPolicy_,
61
+ exposureLimit_,
62
+ wallet_
63
+ );
64
+ }
65
+
66
+ function _newPolicySigned(
67
+ uint256 payout,
68
+ uint256 premium,
69
+ uint256 lossProb,
70
+ uint40 expiration,
71
+ bytes32 policyData,
72
+ bytes32 quoteSignatureR,
73
+ bytes32 quoteSignatureVS,
74
+ uint40 quoteValidUntil,
75
+ address payer,
76
+ address onBehalfOf
77
+ ) internal returns (uint256 policyId) {
78
+ if (!_creationIsOpen)
79
+ _policyPool.access().checkComponentRole(
80
+ address(this),
81
+ POLICY_CREATOR_ROLE,
82
+ _msgSender(),
83
+ false
84
+ );
85
+ require(quoteValidUntil >= block.timestamp, "Quote expired");
86
+
87
+ /**
88
+ * Checks the quote has been signed by an authorized user
89
+ * The "quote" is computed as hash of the following fields (encodePacked):
90
+ * - address(this): the address of this RiskModule
91
+ * - payout, premium, lossProb, expiration: the base parameters of the policy
92
+ * - policyData: a hash of the private details of the policy. The calculation should include some
93
+ * unique id (quoteId), so each policyData identifies a policy.
94
+ * - quoteValidUntil: the maximum validity of the quote
95
+ */
96
+ bytes32 quoteHash = ECDSA.toEthSignedMessageHash(
97
+ abi.encodePacked(
98
+ address(this),
99
+ payout,
100
+ premium,
101
+ lossProb,
102
+ expiration,
103
+ policyData,
104
+ quoteValidUntil
105
+ )
106
+ );
107
+ address signer = ECDSA.recover(quoteHash, quoteSignatureR, quoteSignatureVS);
108
+ _policyPool.access().checkComponentRole(address(this), PRICER_ROLE, signer, false);
109
+ uint96 internalId = uint96(uint256(policyData) % 2**96);
110
+
111
+ policyId = _newPolicy(payout, premium, lossProb, expiration, payer, onBehalfOf, internalId).id;
112
+ emit NewSignedPolicy(policyId, policyData);
113
+ return policyId;
114
+ }
115
+
116
+ /**
117
+  * @dev Creates a new Policy using a signed quote. The caller is the payer of the policy.
118
+  *
119
+  * Requirements:
120
+  * - The caller approved the spending of the premium to the PolicyPool
121
+  * - The quote has been signed by an address with the component role PRICER_ROLE
122
+  *
123
+  * Emits:
124
+  * - {PolicyPool.NewPolicy}
125
+  * - {NewSignedPolicy}
126
+  *
127
+  * @param payout The exposure (maximum payout) of the policy
128
+  * @param premium The premium that will be paid by the payer
129
+  * @param lossProb The probability of having to pay the maximum payout (wad)
130
+  * @param expiration The expiration of the policy (timestamp)
131
+  * @param onBehalfOf The policy holder
132
+  * @param policyData A hash of the private details of the policy. The last 96 bits will be used as internalId
133
+  * @param quoteSignatureR The signature of the "quote". R component (EIP-2098 signature)
134
+  * @param quoteSignatureVS The signature of the "quote". VS component (EIP-2098 signature)
135
+  * @param quoteValidUntil The expiration of the quote
136
+  * @return Returns the id of the created policy
137
+  */
138
+ function newPolicy(
139
+ uint256 payout,
140
+ uint256 premium,
141
+ uint256 lossProb,
142
+ uint40 expiration,
143
+ address onBehalfOf,
144
+ bytes32 policyData,
145
+ bytes32 quoteSignatureR,
146
+ bytes32 quoteSignatureVS,
147
+ uint40 quoteValidUntil
148
+ ) external returns (uint256) {
149
+ return
150
+ _newPolicySigned(
151
+ payout,
152
+ premium,
153
+ lossProb,
154
+ expiration,
155
+ policyData,
156
+ quoteSignatureR,
157
+ quoteSignatureVS,
158
+ quoteValidUntil,
159
+ _msgSender(),
160
+ onBehalfOf
161
+ );
162
+ }
163
+
164
+ /**
165
+  * @dev Creates a new Policy using a signed quote. The payer is the policy holder
166
+  *
167
+  * Requirements:
168
+  * - currency().allowance(onBehalfOf, _msgSender()) > 0
169
+  * - The quote has been signed by an address with the component role PRICER_ROLE
170
+  *
171
+  * Emits:
172
+  * - {PolicyPool.NewPolicy}
173
+  * - {NewSignedPolicy}
174
+  *
175
+  * @param payout The exposure (maximum payout) of the policy
176
+  * @param premium The premium that will be paid by the payer
177
+  * @param lossProb The probability of having to pay the maximum payout (wad)
178
+  * @param expiration The expiration of the policy (timestamp)
179
+  * @param onBehalfOf The policy holder
180
+  * @param policyData A hash of the private details of the policy. The last 96 bits will be used as internalId
181
+  * @param quoteSignatureR The signature of the "quote". R component (EIP-2098 signature)
182
+  * @param quoteSignatureVS The signature of the "quote". VS component (EIP-2098 signature)
183
+  * @param quoteValidUntil The expiration of the quote
184
+  * @return Returns the id of the created policy
185
+  */
186
+ function newPolicyPaidByHolder(
187
+ uint256 payout,
188
+ uint256 premium,
189
+ uint256 lossProb,
190
+ uint40 expiration,
191
+ address onBehalfOf,
192
+ bytes32 policyData,
193
+ bytes32 quoteSignatureR,
194
+ bytes32 quoteSignatureVS,
195
+ uint40 quoteValidUntil
196
+ ) external returns (uint256) {
197
+ require(
198
+ onBehalfOf == _msgSender() || currency().allowance(onBehalfOf, _msgSender()) > 0,
199
+ "Sender is not authorized to create policies onBehalfOf"
200
+ );
201
+ /**
202
+ * The standard is the payer should be the _msgSender() but usually, in this type of module,
203
+ * the sender is an operative account managed by software, where the onBehalfOf is a more
204
+ * secure account (hardware wallet) that does the cash movements.
205
+ * This non standard behaviour allows for a more secure setup, where the sender never manages
206
+ * cash.
207
+ * We leverage the currency's allowance mechanism to allow the sender access to the payer's
208
+ * funds.
209
+ * Note that this allowance won't be spent, so anything above 0 is accepted.
210
+ */
211
+ return
212
+ _newPolicySigned(
213
+ payout,
214
+ premium,
215
+ lossProb,
216
+ expiration,
217
+ policyData,
218
+ quoteSignatureR,
219
+ quoteSignatureVS,
220
+ quoteValidUntil,
221
+ onBehalfOf,
222
+ onBehalfOf
223
+ );
224
+ }
225
+
226
+ function resolvePolicy(Policy.PolicyData calldata policy, uint256 payout)
227
+ external
228
+ onlyComponentRole(RESOLVER_ROLE)
229
+ whenNotPaused
230
+ {
231
+ _policyPool.resolvePolicy(policy, payout);
232
+ }
233
+
234
+ function resolvePolicyFullPayout(Policy.PolicyData calldata policy, bool customerWon)
235
+ external
236
+ onlyComponentRole(RESOLVER_ROLE)
237
+ whenNotPaused
238
+ {
239
+ _policyPool.resolvePolicyFullPayout(policy, customerWon);
240
+ }
241
+ }
@@ -2,7 +2,8 @@
2
2
 
3
3
  pragma solidity ^0.8.0;
4
4
 
5
- import {WadRayMath} from "./WadRayMath.sol";
5
+ import {WadRayMath} from "./dependencies/WadRayMath.sol";
6
+ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
6
7
 
7
8
  /**
8
9
  * @title TimeScaled
@@ -12,13 +13,15 @@ import {WadRayMath} from "./WadRayMath.sol";
12
13
  */
13
14
  library TimeScaled {
14
15
  using WadRayMath for uint256;
16
+ using SafeCast for uint256;
15
17
 
16
- uint256 internal constant SECONDS_PER_YEAR = 365 days;
17
- uint128 public constant MIN_SCALE = 1e17; // 0.0000000001 == 1e-10 in ray
18
+ uint256 private constant SECONDS_PER_YEAR = 365 days;
19
+ uint112 private constant MIN_SCALE = 1e17; // 0.0000000001 == 1e-10 in ray
20
+ uint112 private constant RAY112 = 1e27;
18
21
 
19
22
  struct ScaledAmount {
20
- uint128 scale;
21
- uint96 amount;
23
+ uint112 scale;
24
+ uint112 amount;
22
25
  uint32 lastUpdate;
23
26
  }
24
27
 
@@ -27,7 +30,7 @@ library TimeScaled {
27
30
  if (scaledAmount.amount == 0) {
28
31
  scaledAmount.lastUpdate = uint32(block.timestamp);
29
32
  } else {
30
- scaledAmount.scale = uint128(getScale(scaledAmount, interestRate));
33
+ scaledAmount.scale = getScale(scaledAmount, interestRate).toUint112();
31
34
  scaledAmount.lastUpdate = uint32(block.timestamp);
32
35
  }
33
36
  }
@@ -44,7 +47,7 @@ library TimeScaled {
44
47
  uint256 timeDifference = uint256(now_ - scaledAmount.lastUpdate);
45
48
  return
46
49
  uint256(scaledAmount.scale).rayMul(
47
- ((interestRate.wadToRay() * timeDifference) / SECONDS_PER_YEAR) + WadRayMath.ray()
50
+ ((interestRate.wadToRay() * timeDifference) / SECONDS_PER_YEAR) + WadRayMath.RAY
48
51
  );
49
52
  }
50
53
 
@@ -61,7 +64,7 @@ library TimeScaled {
61
64
  }
62
65
 
63
66
  function init(ScaledAmount storage scaledAmount) internal {
64
- scaledAmount.scale = uint128(WadRayMath.ray());
67
+ scaledAmount.scale = RAY112;
65
68
  scaledAmount.amount = 0;
66
69
  scaledAmount.lastUpdate = uint32(block.timestamp);
67
70
  }
@@ -89,7 +92,7 @@ library TimeScaled {
89
92
  ) internal returns (uint256) {
90
93
  updateScale(scaledAmount, interestRate);
91
94
  uint256 scaledAdd = scaleAmount(scaledAmount, amount);
92
- scaledAmount.amount += uint96(scaledAdd);
95
+ scaledAmount.amount += scaledAdd.toUint96();
93
96
  return scaledAdd;
94
97
  }
95
98
 
@@ -100,10 +103,10 @@ library TimeScaled {
100
103
  ) internal returns (uint256) {
101
104
  updateScale(scaledAmount, interestRate);
102
105
  uint256 scaledSub = scaleAmount(scaledAmount, amount);
103
- scaledAmount.amount -= uint96(scaledSub);
106
+ scaledAmount.amount -= scaledSub.toUint96();
104
107
  if (scaledAmount.amount == 0) {
105
108
  // Reset scale if amount == 0
106
- scaledAmount.scale = uint128(WadRayMath.ray());
109
+ scaledAmount.scale = RAY112;
107
110
  }
108
111
  return scaledSub;
109
112
  }
@@ -115,9 +118,10 @@ library TimeScaled {
115
118
  ) internal {
116
119
  updateScale(scaledAmount, interestRate);
117
120
  uint256 newScaledAmount = uint256(int256(getScaledAmount(scaledAmount, interestRate)) + amount);
118
- scaledAmount.scale = uint128(
119
- newScaledAmount.wadToRay().rayDiv(uint256(scaledAmount.amount).wadToRay())
120
- );
121
+ scaledAmount.scale = newScaledAmount
122
+ .wadToRay()
123
+ .rayDiv(uint256(scaledAmount.amount).wadToRay())
124
+ .toUint112();
121
125
  require(scaledAmount.scale >= MIN_SCALE, "Scale too small, can lead to rounding errors");
122
126
  }
123
127
 
@@ -127,7 +131,7 @@ library TimeScaled {
127
131
  returns (uint256)
128
132
  {
129
133
  uint256 ts = getScaledAmount(scaledAmount, interestRate);
130
- uint256 minTs = uint256(scaledAmount.amount).wadToRay().rayMul(MIN_SCALE * 10).rayToWad();
134
+ uint256 minTs = uint256(scaledAmount.amount).wadToRay().rayMul(MIN_SCALE).rayToWad();
131
135
  if (ts > minTs) return ts - minTs;
132
136
  else return 0;
133
137
  }