@layerzerolabs/utils-upgradeable-evm-contracts 0.2.74

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 (37) hide show
  1. package/.turbo/turbo-lint.log +491 -0
  2. package/.turbo/turbo-test.log +1356 -0
  3. package/LICENSE +23 -0
  4. package/contracts/access/AccessControl2StepUpgradeable.sol +144 -0
  5. package/contracts/allowlist/AllowlistBaseUpgradeable.sol +184 -0
  6. package/contracts/allowlist/AllowlistRBACUpgradeable.sol +55 -0
  7. package/contracts/fee-accounting/FeeHandlerBaseUpgradeable.sol +69 -0
  8. package/contracts/fee-accounting/FeeHandlerRBACUpgradeable.sol +34 -0
  9. package/contracts/fee-config/FeeConfigBaseUpgradeable.sol +128 -0
  10. package/contracts/fee-config/FeeConfigRBACUpgradeable.sol +44 -0
  11. package/contracts/libs/EnumerableSetPagination.sol +77 -0
  12. package/contracts/pause/PauseBaseUpgradeable.sol +107 -0
  13. package/contracts/pause/PauseRBACUpgradeable.sol +47 -0
  14. package/contracts/pause-by-id/PauseByIDBaseUpgradeable.sol +127 -0
  15. package/contracts/pause-by-id/PauseByIDRBACUpgradeable.sol +74 -0
  16. package/contracts/rate-limiter/RateLimiterBaseUpgradeable.sol +636 -0
  17. package/contracts/rate-limiter/RateLimiterRBACUpgradeable.sol +89 -0
  18. package/contracts/rate-limiter/libs/RateLimiterUtils.sol +82 -0
  19. package/foundry.toml +27 -0
  20. package/package.json +34 -0
  21. package/solhint.config.js +3 -0
  22. package/test/AccessControl2StepUpgradeable.t.sol +374 -0
  23. package/test/AllowlistBaseUpgradeable.t.sol +380 -0
  24. package/test/AllowlistRBACUpgradeable.t.sol +84 -0
  25. package/test/EnumerableSetPagination.t.sol +445 -0
  26. package/test/FeeConfigBaseUpgradeable.t.sol +232 -0
  27. package/test/FeeConfigRBACUpgradeable.t.sol +59 -0
  28. package/test/FeeHandlerBaseUpgradeable.t.sol +78 -0
  29. package/test/FeeHandlerRBACUpgradeable.t.sol +45 -0
  30. package/test/PauseBaseUpgradeable.t.sol +244 -0
  31. package/test/PauseByIDBaseUpgradeable.t.sol +337 -0
  32. package/test/PauseByIDRBACUpgradeable.t.sol +414 -0
  33. package/test/PauseRBACUpgradeable.t.sol +258 -0
  34. package/test/RateLimiterBaseUpgradeable.t.sol +826 -0
  35. package/test/RateLimiterRBACUpgradeable.t.sol +128 -0
  36. package/test/RateLimiterRBACUpgradeableInvariant.t.sol +444 -0
  37. package/test/RateLimiterUtils.t.sol +137 -0
@@ -0,0 +1,636 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.22;
3
+
4
+ import { IRateLimiter } from "@layerzerolabs/utils-evm-contracts/contracts/interfaces/IRateLimiter.sol";
5
+ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
6
+ import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
7
+ import { RateLimiterUtils } from "./libs/RateLimiterUtils.sol";
8
+
9
+ /**
10
+ * @title RateLimiterBaseUpgradeable
11
+ * @author LayerZero Labs (tinom.eth)
12
+ * @custom:version 1.0.0
13
+ * @notice Abstract contract that provides toggleable rate limiting functionality for OApps.
14
+ * Token bucket algorithm with linear decay.
15
+ * @dev No public management functions are exposed by this contract, wrappers should be used with access control.
16
+ * Alternatively, refer to `RateLimiterRBACUpgradeable` for a permissioned implementation.
17
+ * @dev Configured limits must be significantly larger than windows to avoid precision loss when calculating decays.
18
+ * @dev Net rate limits offset outflow usage with inflows and vice versa, gross rate limits do not.
19
+ * @dev Amounts are stored as `uint96`, any amounts larger than `type(uint96).max` need to be downscaled via the
20
+ * `SCALE_DECIMALS` constructor parameter.
21
+ * @dev When using global state, ID-specific configs are ignored, and the default config is always used.
22
+ * @dev If `SCALE_DECIMALS` are applied, scaling rounding can favour the user in certain configurations. It is the
23
+ * responsibility of the app to ensure the economic cost of running the rate-limited operation is greater than the
24
+ * economic benefit of bypassing a unit of rate limit.
25
+ *
26
+ * Example 1: Max rate limit reached at beginning of window. As time continues the amount of in flights comes down.
27
+ *
28
+ * Rate Limit Config:
29
+ * limit: 100 units
30
+ * window: 60 seconds
31
+ *
32
+ * Amount in Flight (units) vs. Time Graph (seconds)
33
+ *
34
+ * 100 | * - (Max limit reached at beginning of window)
35
+ * | *
36
+ * | *
37
+ * | *
38
+ * 50 | * (After 30 seconds only 50 units in flight)
39
+ * | *
40
+ * | *
41
+ * | *
42
+ * 0 +--|---|---|---|---|-->(After 60 seconds 0 units are in flight)
43
+ * 0 15 30 45 60 (seconds)
44
+ *
45
+ * Example 2: Max rate limit reached at beginning of window. As time continues the amount of in flights comes down
46
+ * allowing for more to be sent. At the 90 second mark, more in flights come in.
47
+ *
48
+ * Rate Limit Config:
49
+ * limit: 100 units
50
+ * window: 60 seconds
51
+ *
52
+ * Amount in Flight (units) vs. Time Graph (seconds)
53
+ *
54
+ * 100 | * - (Max limit reached at beginning of window)
55
+ * | *
56
+ * | *
57
+ * | *
58
+ * 50 | * * (50 inflight)
59
+ * | * *
60
+ * | * *
61
+ * | * *
62
+ * 0 +--|--|--|--|--|--|--|--|--|--> Time
63
+ * 0 15 30 45 60 75 90 105 120 (seconds)
64
+ *
65
+ * Example 3: Max rate limit reached at beginning of window. At the 15 second mark, the window gets updated to 60
66
+ * seconds and the limit gets updated to 50 units. This scenario shows the direct depiction of "in flight" from the
67
+ * previous window affecting the current window.
68
+ *
69
+ * Initial Rate Limit Config: For first 15 seconds
70
+ * limit: 100 units
71
+ * window: 30 seconds
72
+ *
73
+ * Updated Rate Limit Config: Updated at 15 second mark
74
+ * limit: 50 units
75
+ * window: 60 seconds
76
+ *
77
+ * Amount in Flight (units) vs. Time Graph (seconds)
78
+ * 100 - *
79
+ * |*
80
+ * | *
81
+ * | *
82
+ * | *
83
+ * | *
84
+ * | *
85
+ * 75 - | *
86
+ * | *
87
+ * | *
88
+ * | *
89
+ * | *
90
+ * | *
91
+ * | *
92
+ * | *
93
+ * 50 - | x <--(Slope changes at the 15 second mark because of the update.
94
+ * | o * Window extended to 60 seconds and limit reduced to 50 units.
95
+ * | o * Because amountInFlight/lastUpdated do not reset, 50 units are
96
+ * | o * considered in flight from the previous window and the corresponding
97
+ * | o * decay from the previous rate.)
98
+ * | o *
99
+ * 25 - | o *
100
+ * | o *
101
+ * | o *
102
+ * | o *
103
+ * | o *
104
+ * | o *
105
+ * | o *
106
+ * | o *
107
+ * 0 - +---|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----> Time
108
+ * 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 (seconds)
109
+ * [ Initial 30 Second Window ]
110
+ * [ --------------- Extended 60 Second Window --------------- ]
111
+ */
112
+ abstract contract RateLimiterBaseUpgradeable is Initializable, IRateLimiter {
113
+ /// @notice ID of the default rate limit configuration.
114
+ uint256 public constant DEFAULT_ID = 0;
115
+
116
+ /// @notice Number of decimals to scale the rate limit amounts, usually 0.
117
+ uint8 public immutable SCALE_DECIMALS;
118
+
119
+ /// @dev Factor to scale the rate limit amounts by the scale decimals.
120
+ uint256 internal immutable SCALE_FACTOR;
121
+
122
+ /// @custom:storage-location erc7201:layerzerov2.storage.ratelimiter
123
+ struct RateLimiterStorage {
124
+ bool useGlobalStateFlag;
125
+ bool isGloballyDisabledFlag;
126
+ mapping(uint256 id => RateLimit rateLimit) rateLimits;
127
+ mapping(address user => bool isExempt) isRateLimitAddressExempt;
128
+ }
129
+
130
+ // keccak256(abi.encode(uint256(keccak256("layerzerov2.storage.ratelimiter")) - 1)) & ~bytes32(uint256(0xff))
131
+ bytes32 private constant RATELIMITER_STORAGE_LOCATION =
132
+ 0xfc4b3847e0649a09792d4c694ef28e20c43dde62a8b3de98eff85ccb4e1f3000;
133
+
134
+ /**
135
+ * @notice Internal function to get the rate limiter storage.
136
+ * @return $ Storage pointer
137
+ */
138
+ function _getRateLimiterStorage() internal pure returns (RateLimiterStorage storage $) {
139
+ assembly {
140
+ $.slot := RATELIMITER_STORAGE_LOCATION
141
+ }
142
+ }
143
+
144
+ /**
145
+ * @dev Sets immutable variables.
146
+ * @param _scaleDecimals Number of decimals to scale the rate limit amounts, usually 0
147
+ */
148
+ constructor(uint8 _scaleDecimals) {
149
+ if (_scaleDecimals > 18) revert InvalidScaledDecimals(_scaleDecimals);
150
+ SCALE_DECIMALS = _scaleDecimals;
151
+ SCALE_FACTOR = 10 ** _scaleDecimals;
152
+ }
153
+
154
+ /**
155
+ * @notice Initializes the contract.
156
+ * @param _useGlobalState Whether to use global rules for the rate limiter, instead of per-ID rules
157
+ */
158
+ function __RateLimiterBase_init(bool _useGlobalState) internal onlyInitializing {
159
+ __RateLimiterBase_init_unchained(_useGlobalState);
160
+ }
161
+
162
+ /**
163
+ * @notice Unchained initialization function for the contract.
164
+ * @param _useGlobalState Whether to use global rules for the rate limiter, instead of per-ID rules
165
+ */
166
+ function __RateLimiterBase_init_unchained(bool _useGlobalState) internal onlyInitializing {
167
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
168
+ $.useGlobalStateFlag = _useGlobalState;
169
+
170
+ /// @dev Default config is closed by default.
171
+ $.rateLimits[DEFAULT_ID].configBitmap = RateLimiterUtils.encodeConfigBitmap(false, true, true, true, false);
172
+ emit RateLimitConfigUpdated(
173
+ DEFAULT_ID,
174
+ RateLimitConfig({
175
+ overrideDefaultConfig: false,
176
+ outboundEnabled: true,
177
+ inboundEnabled: true,
178
+ netAccountingEnabled: true,
179
+ addressExemptionEnabled: false,
180
+ outboundLimit: 0,
181
+ inboundLimit: 0,
182
+ outboundWindow: 0,
183
+ inboundWindow: 0
184
+ })
185
+ );
186
+ }
187
+
188
+ // ============ Public Getters ============
189
+
190
+ /**
191
+ * @inheritdoc IRateLimiter
192
+ */
193
+ function getRateLimitGlobalConfig() public view virtual returns (RateLimitGlobalConfig memory globalConfig) {
194
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
195
+ return
196
+ RateLimitGlobalConfig({
197
+ useGlobalState: $.useGlobalStateFlag,
198
+ isGloballyDisabled: $.isGloballyDisabledFlag
199
+ });
200
+ }
201
+
202
+ /**
203
+ * @inheritdoc IRateLimiter
204
+ */
205
+ function rateLimits(uint256 _id) public view virtual returns (RateLimit memory rateLimit) {
206
+ return _getRateLimiterStorage().rateLimits[_id];
207
+ }
208
+
209
+ /**
210
+ * @inheritdoc IRateLimiter
211
+ */
212
+ function isRateLimitAddressExempt(address _user) public view virtual returns (bool isExempt) {
213
+ return _getRateLimiterStorage().isRateLimitAddressExempt[_user];
214
+ }
215
+
216
+ /**
217
+ * @inheritdoc IRateLimiter
218
+ */
219
+ function getRateLimitUsages(
220
+ uint256 _id
221
+ )
222
+ public
223
+ view
224
+ virtual
225
+ returns (
226
+ uint256 outboundUsage,
227
+ uint256 outboundAvailableAmount,
228
+ uint256 inboundUsage,
229
+ uint256 inboundAvailableAmount
230
+ )
231
+ {
232
+ (, RateLimit memory rateLimit, bool outboundEnabled, bool inboundEnabled, , ) = _getRateLimitStateAndConfig(
233
+ _id
234
+ );
235
+ (outboundUsage, outboundAvailableAmount, inboundUsage, inboundAvailableAmount) = _getRateLimitUsages(rateLimit);
236
+ if (SCALE_DECIMALS > 0) {
237
+ (outboundUsage, outboundAvailableAmount, inboundUsage, inboundAvailableAmount) = (
238
+ _upscaleRateLimitAmount(outboundUsage),
239
+ _upscaleRateLimitAmount(outboundAvailableAmount),
240
+ _upscaleRateLimitAmount(inboundUsage),
241
+ _upscaleRateLimitAmount(inboundAvailableAmount)
242
+ );
243
+ }
244
+ if (_getRateLimiterStorage().isGloballyDisabledFlag) {
245
+ (outboundAvailableAmount, inboundAvailableAmount) = (type(uint256).max, type(uint256).max);
246
+ } else {
247
+ if (!outboundEnabled) {
248
+ outboundAvailableAmount = type(uint256).max;
249
+ }
250
+ if (!inboundEnabled) {
251
+ inboundAvailableAmount = type(uint256).max;
252
+ }
253
+ }
254
+ }
255
+
256
+ // ============ Internal API ============
257
+
258
+ /**
259
+ * @notice Applies rate limit logic for an outflow.
260
+ * @dev To be called by the OApp.
261
+ * @param _id ID of the rate limit
262
+ * @param _from Sender of the action
263
+ * @param _amount Amount of the action
264
+ */
265
+ function _outflow(uint256 _id, address _from, uint256 _amount) internal virtual {
266
+ _applyRateLimit(_id, _from, _amount, true);
267
+ }
268
+
269
+ /**
270
+ * @notice Applies rate limit logic for an inflow.
271
+ * @dev To be called by the OApp.
272
+ * @param _id ID of the rate limit
273
+ * @param _to Recipient of the action
274
+ * @param _amount Amount of the action
275
+ */
276
+ function _inflow(uint256 _id, address _to, uint256 _amount) internal virtual {
277
+ _applyRateLimit(_id, _to, _amount, false);
278
+ }
279
+
280
+ // ============ Internal Functions ============
281
+
282
+ /**
283
+ * @notice Applies rate limit logic for an outflow or inflow.
284
+ * @param _id ID of the rate limit
285
+ * @param _user User performing the action
286
+ * @param _amount Amount of the action
287
+ * @param _isOutflow Whether the action is an outflow
288
+ */
289
+ function _applyRateLimit(uint256 _id, address _user, uint256 _amount, bool _isOutflow) internal virtual {
290
+ /// @dev Early return.
291
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
292
+ if ($.isGloballyDisabledFlag) return;
293
+
294
+ /// @dev Optimistically assign outflow directions for outbound and inbound flags.
295
+ /// For outflows, forward is outbound and backward is inbound.
296
+ /// For inflows, forward is inbound and backward is outbound.
297
+ (
298
+ RateLimit storage rateLimitState,
299
+ RateLimit memory rateLimitCache,
300
+ bool forwardEnabled,
301
+ bool backwardEnabled,
302
+ bool netAccountingEnabled,
303
+ bool addressExemptionEnabled
304
+ ) = _getRateLimitStateAndConfig(_id);
305
+
306
+ /// @dev Swap forward and backward flags directions for inflows.
307
+ if (!_isOutflow) {
308
+ (forwardEnabled, backwardEnabled) = (backwardEnabled, forwardEnabled);
309
+ }
310
+
311
+ /// @dev Early returns.
312
+ if (
313
+ (!forwardEnabled && (!backwardEnabled || !netAccountingEnabled)) ||
314
+ (addressExemptionEnabled && isRateLimitAddressExempt(_user))
315
+ ) return;
316
+
317
+ /// @dev Optimistically assign outflow directions.
318
+ (
319
+ uint256 forwardUsage,
320
+ uint256 forwardAvailableAmount,
321
+ uint256 backwardUsage,
322
+ uint256 backwardAvailableAmount
323
+ ) = _getRateLimitUsages(rateLimitCache);
324
+
325
+ /// @dev Swap directions for inflows.
326
+ if (!_isOutflow) {
327
+ (forwardUsage, forwardAvailableAmount, backwardUsage, backwardAvailableAmount) = (
328
+ backwardUsage,
329
+ backwardAvailableAmount,
330
+ forwardUsage,
331
+ forwardAvailableAmount
332
+ );
333
+ }
334
+
335
+ /// @dev Allow downscaling for apps with amounts larger than `type(uint96).max`.
336
+ uint256 amount = SCALE_DECIMALS == 0 ? _amount : _downscaleRateLimitAmount(_amount);
337
+
338
+ if (forwardEnabled) {
339
+ if (forwardAvailableAmount < amount) {
340
+ revert RateLimitExceeded(forwardAvailableAmount, amount);
341
+ }
342
+
343
+ unchecked {
344
+ forwardUsage += amount;
345
+ }
346
+ }
347
+
348
+ if (backwardEnabled && netAccountingEnabled) {
349
+ backwardUsage = Math.saturatingSub(backwardUsage, amount);
350
+ }
351
+
352
+ (rateLimitState.outboundUsage, rateLimitState.inboundUsage) = _isOutflow
353
+ ? (uint96(forwardUsage), uint96(backwardUsage))
354
+ : (uint96(backwardUsage), uint96(forwardUsage));
355
+ rateLimitState.lastUpdated = uint40(block.timestamp);
356
+ }
357
+
358
+ /**
359
+ * @notice Gets the rate limit state and configuration for a given ID, falling back to defaults if necessary.
360
+ * @param _id ID of the rate limit
361
+ * @return rateLimitState Rate limit state
362
+ * @return rateLimitCache Rate limit configuration
363
+ * @return outboundEnabled Whether outbound is enabled
364
+ * @return inboundEnabled Whether inbound is enabled
365
+ * @return netAccountingEnabled Whether net accounting is enabled
366
+ * @return addressExemptionEnabled Whether address exemption is enabled
367
+ */
368
+ function _getRateLimitStateAndConfig(
369
+ uint256 _id
370
+ )
371
+ internal
372
+ view
373
+ virtual
374
+ returns (
375
+ RateLimit storage rateLimitState,
376
+ RateLimit memory rateLimitCache,
377
+ bool outboundEnabled,
378
+ bool inboundEnabled,
379
+ bool netAccountingEnabled,
380
+ bool addressExemptionEnabled
381
+ )
382
+ {
383
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
384
+
385
+ uint256 stateId;
386
+ uint256 configId;
387
+
388
+ if ($.useGlobalStateFlag) {
389
+ /// @dev If using global state, ID-specific config is ignored.
390
+ stateId = DEFAULT_ID;
391
+ configId = DEFAULT_ID;
392
+ } else {
393
+ stateId = _id;
394
+ configId = RateLimiterUtils.decodeOverrideDefaultConfig($.rateLimits[_id].configBitmap) ? _id : DEFAULT_ID;
395
+ }
396
+
397
+ rateLimitState = $.rateLimits[stateId];
398
+ rateLimitCache = _populateRateLimitCache(stateId, configId);
399
+
400
+ (outboundEnabled, inboundEnabled, netAccountingEnabled, addressExemptionEnabled) = RateLimiterUtils
401
+ .decodeConfigBitmapFlags(rateLimitCache.configBitmap);
402
+ }
403
+
404
+ /**
405
+ * @notice Populates the rate limit cache for a given state and configuration ID.
406
+ * @dev Avoids stack too deep error in `_getRateLimitStateAndConfig`.
407
+ * @param _stateId State ID
408
+ * @param _configId Configuration ID
409
+ * @return rateLimitCache Rate limit cache
410
+ */
411
+ function _populateRateLimitCache(
412
+ uint256 _stateId,
413
+ uint256 _configId
414
+ ) internal view virtual returns (RateLimit memory rateLimitCache) {
415
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
416
+
417
+ (rateLimitCache.outboundUsage, rateLimitCache.inboundUsage, rateLimitCache.lastUpdated) = (
418
+ $.rateLimits[_stateId].outboundUsage,
419
+ $.rateLimits[_stateId].inboundUsage,
420
+ $.rateLimits[_stateId].lastUpdated
421
+ );
422
+
423
+ (
424
+ rateLimitCache.configBitmap,
425
+ rateLimitCache.outboundLimit,
426
+ rateLimitCache.inboundLimit,
427
+ rateLimitCache.outboundWindow,
428
+ rateLimitCache.inboundWindow
429
+ ) = (
430
+ $.rateLimits[_configId].configBitmap,
431
+ $.rateLimits[_configId].outboundLimit,
432
+ $.rateLimits[_configId].inboundLimit,
433
+ $.rateLimits[_configId].outboundWindow,
434
+ $.rateLimits[_configId].inboundWindow
435
+ );
436
+ }
437
+
438
+ /**
439
+ * @notice Calculates decayed usages and remaining capacities for a rate limit.
440
+ * @param _rateLimit Rate limit state to calculate usages for
441
+ * @return outboundUsage Current usage of the outbound rate limit
442
+ * @return outboundAvailableAmount Remaining capacity of the outbound rate limit
443
+ * @return inboundUsage Current usage of the inbound rate limit
444
+ * @return inboundAvailableAmount Remaining capacity of the inbound rate limit
445
+ */
446
+ function _getRateLimitUsages(
447
+ RateLimit memory _rateLimit
448
+ )
449
+ internal
450
+ view
451
+ virtual
452
+ returns (
453
+ uint256 outboundUsage,
454
+ uint256 outboundAvailableAmount,
455
+ uint256 inboundUsage,
456
+ uint256 inboundAvailableAmount
457
+ )
458
+ {
459
+ (outboundUsage, outboundAvailableAmount) = _getRateLimitUsage(
460
+ _rateLimit.lastUpdated,
461
+ _rateLimit.outboundUsage,
462
+ _rateLimit.outboundLimit,
463
+ _rateLimit.outboundWindow
464
+ );
465
+ (inboundUsage, inboundAvailableAmount) = _getRateLimitUsage(
466
+ _rateLimit.lastUpdated,
467
+ _rateLimit.inboundUsage,
468
+ _rateLimit.inboundLimit,
469
+ _rateLimit.inboundWindow
470
+ );
471
+ }
472
+
473
+ /**
474
+ * @notice Calculates decayed usage and remaining capacity for a rate limit direction.
475
+ * @dev Treats 0-windows as 1-second windows.
476
+ * @dev Decay can be stalled due to precision loss if `_limit` is not significantly larger than `_window`.
477
+ * @param _lastUpdated Last updated timestamp
478
+ * @param _amountInFlight Amount in flight
479
+ * @param _limit Limit of the rate limit
480
+ * @param _window Window of the rate limit
481
+ * @return currentUsage Current usage of the rate limit
482
+ * @return availableAmount Remaining capacity of the rate limit
483
+ */
484
+ function _getRateLimitUsage(
485
+ uint40 _lastUpdated,
486
+ uint96 _amountInFlight,
487
+ uint96 _limit,
488
+ uint32 _window
489
+ ) internal view virtual returns (uint256 currentUsage, uint256 availableAmount) {
490
+ uint256 timeSinceLastUpdate = block.timestamp - _lastUpdated;
491
+ unchecked {
492
+ uint256 decay = (_limit * timeSinceLastUpdate) / (_window > 0 ? _window : 1);
493
+ currentUsage = Math.saturatingSub(_amountInFlight, decay);
494
+ availableAmount = Math.saturatingSub(_limit, currentUsage);
495
+ }
496
+ }
497
+
498
+ /**
499
+ * @notice Calculates decayed usages and updates state for a rate limit.
500
+ * @dev To be called before updating new limits and windows.
501
+ * @param _id ID of the rate limit
502
+ */
503
+ function _checkpointRateLimit(uint256 _id) internal virtual {
504
+ (RateLimit storage rateLimitState, RateLimit memory rateLimitCache, , , , ) = _getRateLimitStateAndConfig(_id);
505
+
506
+ (uint256 outboundUsage, , uint256 inboundUsage, ) = _getRateLimitUsages(rateLimitCache);
507
+
508
+ rateLimitState.outboundUsage = uint96(outboundUsage);
509
+ rateLimitState.inboundUsage = uint96(inboundUsage);
510
+ rateLimitState.lastUpdated = uint40(block.timestamp);
511
+ }
512
+
513
+ // ============ Internal Functions to Wrap with Access Control ============
514
+
515
+ /**
516
+ * @notice Checkpoints rate limits for multiple IDs, updating decayed usages to storage.
517
+ * @dev To be called before updating new limits and windows.
518
+ * @param _ids Array of rate limit IDs to checkpoint
519
+ */
520
+ function _checkpointRateLimits(uint256[] calldata _ids) internal virtual {
521
+ for (uint256 i = 0; i < _ids.length; i++) {
522
+ _checkpointRateLimit(_ids[i]);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * @notice Internal function to set the global configuration for the rate limiter.
528
+ * @dev To be wrapped with access control.
529
+ * @param _globalConfig Global configuration to set
530
+ */
531
+ function _setRateLimitGlobalConfig(RateLimitGlobalConfig memory _globalConfig) internal virtual {
532
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
533
+ $.useGlobalStateFlag = _globalConfig.useGlobalState;
534
+ $.isGloballyDisabledFlag = _globalConfig.isGloballyDisabled;
535
+ emit RateLimitGlobalConfigUpdated(_globalConfig);
536
+ }
537
+
538
+ /**
539
+ * @notice Internal function to set ID-specific configurations for the rate limiter.
540
+ * @dev To be wrapped with access control.
541
+ * @dev Configurations must be significantly larger than windows to avoid precision loss when calculating decays.
542
+ * @param _params Array of configurations to set
543
+ */
544
+ function _setRateLimitConfigs(SetRateLimitConfigParam[] calldata _params) internal virtual {
545
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
546
+
547
+ for (uint256 i = 0; i < _params.length; i++) {
548
+ SetRateLimitConfigParam calldata param = _params[i];
549
+ RateLimit storage rateLimit = $.rateLimits[param.id];
550
+
551
+ rateLimit.configBitmap = RateLimiterUtils.encodeConfigBitmap(
552
+ param.config.overrideDefaultConfig,
553
+ param.config.outboundEnabled,
554
+ param.config.inboundEnabled,
555
+ param.config.netAccountingEnabled,
556
+ param.config.addressExemptionEnabled
557
+ );
558
+ rateLimit.outboundLimit = param.config.outboundLimit;
559
+ rateLimit.inboundLimit = param.config.inboundLimit;
560
+ rateLimit.outboundWindow = param.config.outboundWindow;
561
+ rateLimit.inboundWindow = param.config.inboundWindow;
562
+
563
+ emit RateLimitConfigUpdated(param.id, param.config);
564
+ }
565
+ }
566
+
567
+ /**
568
+ * @notice Internal function to set ID-specific states for the rate limiter.
569
+ * @dev To be wrapped with access control.
570
+ * @dev States cannot be set to a timestamp in the future.
571
+ * @param _params Array of states to set
572
+ */
573
+ function _setRateLimitStates(SetRateLimitStateParam[] calldata _params) internal virtual {
574
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
575
+
576
+ for (uint256 i = 0; i < _params.length; i++) {
577
+ SetRateLimitStateParam calldata param = _params[i];
578
+ RateLimit storage rateLimit = $.rateLimits[param.id];
579
+
580
+ if (param.state.lastUpdated > block.timestamp) {
581
+ revert LastUpdatedInFuture(param.state.lastUpdated, uint40(block.timestamp));
582
+ }
583
+
584
+ rateLimit.outboundUsage = param.state.outboundUsage;
585
+ rateLimit.inboundUsage = param.state.inboundUsage;
586
+ rateLimit.lastUpdated = param.state.lastUpdated;
587
+
588
+ emit RateLimitStateUpdated(param.id, param.state);
589
+ }
590
+ }
591
+
592
+ /**
593
+ * @notice Internal function to set address exemptions for the rate limiter.
594
+ * @dev To be wrapped with access control.
595
+ * @dev Only in effect if `addressExemptionEnabled` is true for an ID.
596
+ * @param _exemptions Array of exemptions to set
597
+ */
598
+ function _setRateLimitAddressExemptions(SetRateLimitAddressExemptionParam[] calldata _exemptions) internal virtual {
599
+ RateLimiterStorage storage $ = _getRateLimiterStorage();
600
+
601
+ for (uint256 i = 0; i < _exemptions.length; i++) {
602
+ SetRateLimitAddressExemptionParam calldata exemption = _exemptions[i];
603
+
604
+ if ($.isRateLimitAddressExempt[exemption.user] == exemption.isExempt) {
605
+ revert ExemptionStateIdempotent(exemption.user, exemption.isExempt);
606
+ }
607
+
608
+ $.isRateLimitAddressExempt[exemption.user] = exemption.isExempt;
609
+
610
+ emit RateLimitAddressExemptionUpdated(exemption.user, exemption.isExempt);
611
+ }
612
+ }
613
+
614
+ // ============ Up/Down Scaling ============
615
+
616
+ /**
617
+ * @notice Downscales the rate limit amount by the scale decimals.
618
+ * @param _amount Amount to downscale
619
+ * @return Downscaled amount
620
+ */
621
+ function _downscaleRateLimitAmount(uint256 _amount) internal view virtual returns (uint256) {
622
+ return Math.ceilDiv(_amount, SCALE_FACTOR);
623
+ }
624
+
625
+ /**
626
+ * @notice Upscales the rate limit amount by the scale decimals.
627
+ * @param _amount Amount to upscale
628
+ * @return Upscaled amount
629
+ */
630
+ function _upscaleRateLimitAmount(uint256 _amount) internal view virtual returns (uint256) {
631
+ /// @dev Safe since maximum value is `type(uint96).max` for `_amount`, and `10 ** 18` for `SCALE_FACTOR`.
632
+ unchecked {
633
+ return _amount * SCALE_FACTOR;
634
+ }
635
+ }
636
+ }