@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.
- package/.turbo/turbo-lint.log +491 -0
- package/.turbo/turbo-test.log +1356 -0
- package/LICENSE +23 -0
- package/contracts/access/AccessControl2StepUpgradeable.sol +144 -0
- package/contracts/allowlist/AllowlistBaseUpgradeable.sol +184 -0
- package/contracts/allowlist/AllowlistRBACUpgradeable.sol +55 -0
- package/contracts/fee-accounting/FeeHandlerBaseUpgradeable.sol +69 -0
- package/contracts/fee-accounting/FeeHandlerRBACUpgradeable.sol +34 -0
- package/contracts/fee-config/FeeConfigBaseUpgradeable.sol +128 -0
- package/contracts/fee-config/FeeConfigRBACUpgradeable.sol +44 -0
- package/contracts/libs/EnumerableSetPagination.sol +77 -0
- package/contracts/pause/PauseBaseUpgradeable.sol +107 -0
- package/contracts/pause/PauseRBACUpgradeable.sol +47 -0
- package/contracts/pause-by-id/PauseByIDBaseUpgradeable.sol +127 -0
- package/contracts/pause-by-id/PauseByIDRBACUpgradeable.sol +74 -0
- package/contracts/rate-limiter/RateLimiterBaseUpgradeable.sol +636 -0
- package/contracts/rate-limiter/RateLimiterRBACUpgradeable.sol +89 -0
- package/contracts/rate-limiter/libs/RateLimiterUtils.sol +82 -0
- package/foundry.toml +27 -0
- package/package.json +34 -0
- package/solhint.config.js +3 -0
- package/test/AccessControl2StepUpgradeable.t.sol +374 -0
- package/test/AllowlistBaseUpgradeable.t.sol +380 -0
- package/test/AllowlistRBACUpgradeable.t.sol +84 -0
- package/test/EnumerableSetPagination.t.sol +445 -0
- package/test/FeeConfigBaseUpgradeable.t.sol +232 -0
- package/test/FeeConfigRBACUpgradeable.t.sol +59 -0
- package/test/FeeHandlerBaseUpgradeable.t.sol +78 -0
- package/test/FeeHandlerRBACUpgradeable.t.sol +45 -0
- package/test/PauseBaseUpgradeable.t.sol +244 -0
- package/test/PauseByIDBaseUpgradeable.t.sol +337 -0
- package/test/PauseByIDRBACUpgradeable.t.sol +414 -0
- package/test/PauseRBACUpgradeable.t.sol +258 -0
- package/test/RateLimiterBaseUpgradeable.t.sol +826 -0
- package/test/RateLimiterRBACUpgradeable.t.sol +128 -0
- package/test/RateLimiterRBACUpgradeableInvariant.t.sol +444 -0
- 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
|
+
}
|