@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,128 @@
|
|
|
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 { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol";
|
|
6
|
+
import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
|
|
7
|
+
import { RateLimiterRBACUpgradeable } from "./../contracts/rate-limiter/RateLimiterRBACUpgradeable.sol";
|
|
8
|
+
import { RateLimiterBaseUpgradeableTest, RateLimiterBaseUpgradeableMock } from "./RateLimiterBaseUpgradeable.t.sol";
|
|
9
|
+
|
|
10
|
+
contract RateLimiterRBACUpgradeableMock is RateLimiterRBACUpgradeable {
|
|
11
|
+
constructor(uint8 _scaleDecimals) RateLimiterRBACUpgradeable(_scaleDecimals) {
|
|
12
|
+
_disableInitializers();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function initialize(bool _useGlobalState, address _initialAdmin) public initializer {
|
|
16
|
+
__RateLimiterBase_init(_useGlobalState);
|
|
17
|
+
__AccessControl2Step_init(_initialAdmin);
|
|
18
|
+
_grantRole(RATE_LIMITER_MANAGER_ROLE, _initialAdmin);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function outflow(uint256 _id, address _from, uint256 _amount) external {
|
|
22
|
+
_outflow(_id, _from, _amount);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function inflow(uint256 _id, address _to, uint256 _amount) external {
|
|
26
|
+
_inflow(_id, _to, _amount);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function upscaleRateLimitAmount(uint256 _amount) external view returns (uint256) {
|
|
30
|
+
return _upscaleRateLimitAmount(_amount);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function downscaleRateLimitAmount(uint256 _amount) external view returns (uint256) {
|
|
34
|
+
return _downscaleRateLimitAmount(_amount);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
contract RateLimiterRBACUpgradeableTest is RateLimiterBaseUpgradeableTest {
|
|
39
|
+
bytes32 constant RATE_LIMITER_MANAGER_ROLE = keccak256("RATE_LIMITER_MANAGER_ROLE");
|
|
40
|
+
|
|
41
|
+
address charlie = makeAddr("charlie");
|
|
42
|
+
|
|
43
|
+
function _createRateLimiter(
|
|
44
|
+
uint8 _scaleDecimals,
|
|
45
|
+
bool _useGlobalState
|
|
46
|
+
) internal virtual override returns (RateLimiterBaseUpgradeableMock) {
|
|
47
|
+
RateLimiterRBACUpgradeableMock impl = new RateLimiterRBACUpgradeableMock(_scaleDecimals);
|
|
48
|
+
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
|
|
49
|
+
address(impl),
|
|
50
|
+
address(this),
|
|
51
|
+
abi.encodeWithSelector(RateLimiterRBACUpgradeableMock.initialize.selector, _useGlobalState, address(this))
|
|
52
|
+
);
|
|
53
|
+
return RateLimiterBaseUpgradeableMock(address(proxy));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function test_setRateLimitGlobalConfig_Revert_Unauthorized() public {
|
|
57
|
+
IRateLimiter.RateLimitGlobalConfig memory config = IRateLimiter.RateLimitGlobalConfig({
|
|
58
|
+
useGlobalState: true,
|
|
59
|
+
isGloballyDisabled: false
|
|
60
|
+
});
|
|
61
|
+
vm.expectRevert(
|
|
62
|
+
abi.encodeWithSelector(
|
|
63
|
+
IAccessControl.AccessControlUnauthorizedAccount.selector,
|
|
64
|
+
charlie,
|
|
65
|
+
RATE_LIMITER_MANAGER_ROLE
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
vm.prank(charlie);
|
|
69
|
+
RateLimiterRBACUpgradeableMock(address(rateLimiter)).setRateLimitGlobalConfig(config);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function test_setRateLimitConfigs_Revert_Unauthorized() public {
|
|
73
|
+
IRateLimiter.SetRateLimitConfigParam[] memory configs = new IRateLimiter.SetRateLimitConfigParam[](1);
|
|
74
|
+
configs[0] = _createConfig(ID_1, true, true, true, false, false, 1000, 1000, 1 days, 1 days);
|
|
75
|
+
vm.expectRevert(
|
|
76
|
+
abi.encodeWithSelector(
|
|
77
|
+
IAccessControl.AccessControlUnauthorizedAccount.selector,
|
|
78
|
+
charlie,
|
|
79
|
+
RATE_LIMITER_MANAGER_ROLE
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
vm.prank(charlie);
|
|
83
|
+
RateLimiterRBACUpgradeableMock(address(rateLimiter)).setRateLimitConfigs(configs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function test_setRateLimitStates_Revert_Unauthorized() public {
|
|
87
|
+
IRateLimiter.SetRateLimitStateParam[] memory states = new IRateLimiter.SetRateLimitStateParam[](1);
|
|
88
|
+
states[0] = _createState(ID_1, 100, 100, uint40(block.timestamp));
|
|
89
|
+
vm.expectRevert(
|
|
90
|
+
abi.encodeWithSelector(
|
|
91
|
+
IAccessControl.AccessControlUnauthorizedAccount.selector,
|
|
92
|
+
charlie,
|
|
93
|
+
RATE_LIMITER_MANAGER_ROLE
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
vm.prank(charlie);
|
|
97
|
+
RateLimiterRBACUpgradeableMock(address(rateLimiter)).setRateLimitStates(states);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function test_setRateLimitAddressExemptions_Revert_Unauthorized() public {
|
|
101
|
+
IRateLimiter.SetRateLimitAddressExemptionParam[]
|
|
102
|
+
memory exemptions = new IRateLimiter.SetRateLimitAddressExemptionParam[](1);
|
|
103
|
+
exemptions[0] = IRateLimiter.SetRateLimitAddressExemptionParam({ user: address(0x123), isExempt: true });
|
|
104
|
+
vm.expectRevert(
|
|
105
|
+
abi.encodeWithSelector(
|
|
106
|
+
IAccessControl.AccessControlUnauthorizedAccount.selector,
|
|
107
|
+
charlie,
|
|
108
|
+
RATE_LIMITER_MANAGER_ROLE
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
vm.prank(charlie);
|
|
112
|
+
RateLimiterRBACUpgradeableMock(address(rateLimiter)).setRateLimitAddressExemptions(exemptions);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function test_checkpointRateLimits_Revert_Unauthorized() public {
|
|
116
|
+
uint256[] memory ids = new uint256[](1);
|
|
117
|
+
ids[0] = ID_1;
|
|
118
|
+
vm.expectRevert(
|
|
119
|
+
abi.encodeWithSelector(
|
|
120
|
+
IAccessControl.AccessControlUnauthorizedAccount.selector,
|
|
121
|
+
charlie,
|
|
122
|
+
RATE_LIMITER_MANAGER_ROLE
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
vm.prank(charlie);
|
|
126
|
+
RateLimiterRBACUpgradeableMock(address(rateLimiter)).checkpointRateLimits(ids);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
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 { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
|
|
6
|
+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
|
|
7
|
+
import { StdInvariant } from "forge-std/StdInvariant.sol";
|
|
8
|
+
import { Test } from "forge-std/Test.sol";
|
|
9
|
+
import { RateLimiterRBACUpgradeable } from "./../contracts/rate-limiter/RateLimiterRBACUpgradeable.sol";
|
|
10
|
+
|
|
11
|
+
contract RateLimiterRBACUpgradeableFuzzMock is RateLimiterRBACUpgradeable {
|
|
12
|
+
constructor(uint8 _scaleDecimals) RateLimiterRBACUpgradeable(_scaleDecimals) {
|
|
13
|
+
_disableInitializers();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function initialize(bool _useGlobalState, address _initialAdmin) public initializer {
|
|
17
|
+
__RateLimiterBase_init(_useGlobalState);
|
|
18
|
+
__AccessControl2Step_init(_initialAdmin);
|
|
19
|
+
_grantRole(RATE_LIMITER_MANAGER_ROLE, _initialAdmin);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function outflow(uint256 _id, address _from, uint256 _amount) public {
|
|
23
|
+
_outflow(_id, _from, _amount);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function inflow(uint256 _id, address _to, uint256 _amount) public {
|
|
27
|
+
_inflow(_id, _to, _amount);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
contract RateLimiterRBACUpgradeableHandler is Test {
|
|
32
|
+
RateLimiterRBACUpgradeableFuzzMock public limiter;
|
|
33
|
+
|
|
34
|
+
uint256 public immutable ID;
|
|
35
|
+
address public constant USER = address(0x123);
|
|
36
|
+
address public constant EXEMPT_USER = address(0x999);
|
|
37
|
+
|
|
38
|
+
uint96 public currentOutboundLimit;
|
|
39
|
+
uint96 public currentInboundLimit;
|
|
40
|
+
uint32 public currentOutboundWindow;
|
|
41
|
+
uint32 public currentInboundWindow;
|
|
42
|
+
bool public netAccounting;
|
|
43
|
+
bool public addressExemptionEnabled;
|
|
44
|
+
|
|
45
|
+
uint256 public ghost_outboundUsage;
|
|
46
|
+
uint256 public ghost_inboundUsage;
|
|
47
|
+
uint40 public ghost_lastUpdated;
|
|
48
|
+
bool public ghost_exemptUserIsExempt;
|
|
49
|
+
bool public ghost_globallyDisabled;
|
|
50
|
+
bool public ghost_revertMismatch;
|
|
51
|
+
|
|
52
|
+
constructor(RateLimiterRBACUpgradeableFuzzMock _limiter, uint256 _id) {
|
|
53
|
+
limiter = _limiter;
|
|
54
|
+
ID = _id;
|
|
55
|
+
|
|
56
|
+
currentOutboundLimit = 1000 ether;
|
|
57
|
+
currentInboundLimit = 1000 ether;
|
|
58
|
+
currentOutboundWindow = 1000;
|
|
59
|
+
currentInboundWindow = 1000;
|
|
60
|
+
netAccounting = false;
|
|
61
|
+
addressExemptionEnabled = true;
|
|
62
|
+
|
|
63
|
+
ghost_lastUpdated = uint40(block.timestamp);
|
|
64
|
+
ghost_exemptUserIsExempt = true;
|
|
65
|
+
|
|
66
|
+
vm.startPrank(msg.sender);
|
|
67
|
+
_applyConfig(
|
|
68
|
+
currentOutboundLimit,
|
|
69
|
+
currentInboundLimit,
|
|
70
|
+
currentOutboundWindow,
|
|
71
|
+
currentInboundWindow,
|
|
72
|
+
netAccounting,
|
|
73
|
+
addressExemptionEnabled
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
IRateLimiter.SetRateLimitAddressExemptionParam[]
|
|
77
|
+
memory exemptions = new IRateLimiter.SetRateLimitAddressExemptionParam[](1);
|
|
78
|
+
exemptions[0] = IRateLimiter.SetRateLimitAddressExemptionParam({ user: EXEMPT_USER, isExempt: true });
|
|
79
|
+
limiter.setRateLimitAddressExemptions(exemptions);
|
|
80
|
+
vm.stopPrank();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _applyConfig(
|
|
84
|
+
uint96 outLimit,
|
|
85
|
+
uint96 inLimit,
|
|
86
|
+
uint32 outWindow,
|
|
87
|
+
uint32 inWindow,
|
|
88
|
+
bool newNet,
|
|
89
|
+
bool newExemptionEnabled
|
|
90
|
+
) internal {
|
|
91
|
+
// Checkpoint ghost with old config, then checkpoint on-chain before setting new config.
|
|
92
|
+
_updateGhostDecay();
|
|
93
|
+
|
|
94
|
+
uint256[] memory ids = new uint256[](1);
|
|
95
|
+
ids[0] = ID;
|
|
96
|
+
limiter.checkpointRateLimits(ids);
|
|
97
|
+
|
|
98
|
+
IRateLimiter.SetRateLimitConfigParam[] memory configs = new IRateLimiter.SetRateLimitConfigParam[](1);
|
|
99
|
+
configs[0] = IRateLimiter.SetRateLimitConfigParam({
|
|
100
|
+
id: ID,
|
|
101
|
+
config: IRateLimiter.RateLimitConfig({
|
|
102
|
+
overrideDefaultConfig: true,
|
|
103
|
+
outboundEnabled: true,
|
|
104
|
+
inboundEnabled: true,
|
|
105
|
+
netAccountingEnabled: newNet,
|
|
106
|
+
addressExemptionEnabled: newExemptionEnabled,
|
|
107
|
+
outboundLimit: outLimit,
|
|
108
|
+
inboundLimit: inLimit,
|
|
109
|
+
outboundWindow: outWindow,
|
|
110
|
+
inboundWindow: inWindow
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
limiter.setRateLimitConfigs(configs);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _updateGhostDecay() internal {
|
|
117
|
+
uint256 timeSince = block.timestamp - ghost_lastUpdated;
|
|
118
|
+
|
|
119
|
+
uint256 outDecay = (uint256(currentOutboundLimit) * timeSince) /
|
|
120
|
+
(currentOutboundWindow > 0 ? currentOutboundWindow : 1);
|
|
121
|
+
uint256 inDecay = (uint256(currentInboundLimit) * timeSince) /
|
|
122
|
+
(currentInboundWindow > 0 ? currentInboundWindow : 1);
|
|
123
|
+
|
|
124
|
+
if (outDecay > 0) ghost_outboundUsage = Math.saturatingSub(ghost_outboundUsage, outDecay);
|
|
125
|
+
if (inDecay > 0) ghost_inboundUsage = Math.saturatingSub(ghost_inboundUsage, inDecay);
|
|
126
|
+
|
|
127
|
+
ghost_lastUpdated = uint40(block.timestamp);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getGhostUsages() public view returns (uint256 outbound, uint256 inbound) {
|
|
131
|
+
uint256 timeSince = block.timestamp - ghost_lastUpdated;
|
|
132
|
+
|
|
133
|
+
uint256 outDecay = (uint256(currentOutboundLimit) * timeSince) /
|
|
134
|
+
(currentOutboundWindow > 0 ? currentOutboundWindow : 1);
|
|
135
|
+
uint256 inDecay = (uint256(currentInboundLimit) * timeSince) /
|
|
136
|
+
(currentInboundWindow > 0 ? currentInboundWindow : 1);
|
|
137
|
+
|
|
138
|
+
outbound = Math.saturatingSub(ghost_outboundUsage, outDecay);
|
|
139
|
+
inbound = Math.saturatingSub(ghost_inboundUsage, inDecay);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function outflow(uint96 amount, bool useExempt) public {
|
|
143
|
+
amount = uint96(bound(amount, 1, currentOutboundLimit * 2));
|
|
144
|
+
address user = useExempt ? EXEMPT_USER : USER;
|
|
145
|
+
|
|
146
|
+
if (ghost_globallyDisabled) {
|
|
147
|
+
try limiter.outflow(ID, user, amount) {} catch {
|
|
148
|
+
ghost_revertMismatch = true;
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
bool isExempt = useExempt && addressExemptionEnabled && ghost_exemptUserIsExempt;
|
|
154
|
+
|
|
155
|
+
(, uint256 outAvail, , ) = limiter.getRateLimitUsages(ID);
|
|
156
|
+
bool shouldSucceed = isExempt || amount <= outAvail;
|
|
157
|
+
|
|
158
|
+
try limiter.outflow(ID, user, amount) {
|
|
159
|
+
if (!shouldSucceed) ghost_revertMismatch = true;
|
|
160
|
+
if (!isExempt) {
|
|
161
|
+
_updateGhostDecay();
|
|
162
|
+
ghost_outboundUsage += amount;
|
|
163
|
+
if (netAccounting) {
|
|
164
|
+
ghost_inboundUsage = Math.saturatingSub(ghost_inboundUsage, amount);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (bytes memory reason) {
|
|
168
|
+
if (shouldSucceed) {
|
|
169
|
+
ghost_revertMismatch = true;
|
|
170
|
+
} else {
|
|
171
|
+
bytes memory expected = abi.encodeWithSelector(
|
|
172
|
+
IRateLimiter.RateLimitExceeded.selector,
|
|
173
|
+
outAvail,
|
|
174
|
+
amount
|
|
175
|
+
);
|
|
176
|
+
if (keccak256(reason) != keccak256(expected)) ghost_revertMismatch = true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function inflow(uint96 amount, bool useExempt) public {
|
|
182
|
+
amount = uint96(bound(amount, 1, currentInboundLimit * 2));
|
|
183
|
+
address user = useExempt ? EXEMPT_USER : USER;
|
|
184
|
+
|
|
185
|
+
if (ghost_globallyDisabled) {
|
|
186
|
+
try limiter.inflow(ID, user, amount) {} catch {
|
|
187
|
+
ghost_revertMismatch = true;
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
bool isExempt = useExempt && addressExemptionEnabled && ghost_exemptUserIsExempt;
|
|
193
|
+
|
|
194
|
+
(, , , uint256 inAvail) = limiter.getRateLimitUsages(ID);
|
|
195
|
+
bool shouldSucceed = isExempt || amount <= inAvail;
|
|
196
|
+
|
|
197
|
+
try limiter.inflow(ID, user, amount) {
|
|
198
|
+
if (!shouldSucceed) ghost_revertMismatch = true;
|
|
199
|
+
if (!isExempt) {
|
|
200
|
+
_updateGhostDecay();
|
|
201
|
+
ghost_inboundUsage += amount;
|
|
202
|
+
if (netAccounting) {
|
|
203
|
+
ghost_outboundUsage = Math.saturatingSub(ghost_outboundUsage, amount);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (bytes memory reason) {
|
|
207
|
+
if (shouldSucceed) {
|
|
208
|
+
ghost_revertMismatch = true;
|
|
209
|
+
} else {
|
|
210
|
+
bytes memory expected = abi.encodeWithSelector(
|
|
211
|
+
IRateLimiter.RateLimitExceeded.selector,
|
|
212
|
+
inAvail,
|
|
213
|
+
amount
|
|
214
|
+
);
|
|
215
|
+
if (keccak256(reason) != keccak256(expected)) ghost_revertMismatch = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function warp(uint32 seconds_) public {
|
|
221
|
+
uint32 maxWindow = currentOutboundWindow > currentInboundWindow ? currentOutboundWindow : currentInboundWindow;
|
|
222
|
+
seconds_ = uint32(bound(seconds_, 1, maxWindow * 2));
|
|
223
|
+
vm.warp(block.timestamp + seconds_);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function setConfig(
|
|
227
|
+
uint96 outLimit,
|
|
228
|
+
uint96 inLimit,
|
|
229
|
+
uint32 outWindow,
|
|
230
|
+
uint32 inWindow,
|
|
231
|
+
bool net,
|
|
232
|
+
bool exemptionEnabled
|
|
233
|
+
) public {
|
|
234
|
+
outLimit = uint96(bound(outLimit, 100 ether, 10000 ether));
|
|
235
|
+
inLimit = uint96(bound(inLimit, 100 ether, 10000 ether));
|
|
236
|
+
outWindow = uint32(bound(outWindow, 10, 10000));
|
|
237
|
+
inWindow = uint32(bound(inWindow, 10, 10000));
|
|
238
|
+
|
|
239
|
+
_applyConfig(outLimit, inLimit, outWindow, inWindow, net, exemptionEnabled);
|
|
240
|
+
|
|
241
|
+
currentOutboundLimit = outLimit;
|
|
242
|
+
currentInboundLimit = inLimit;
|
|
243
|
+
currentOutboundWindow = outWindow;
|
|
244
|
+
currentInboundWindow = inWindow;
|
|
245
|
+
netAccounting = net;
|
|
246
|
+
addressExemptionEnabled = exemptionEnabled;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function toggleExemption() public {
|
|
250
|
+
bool newExempt = !ghost_exemptUserIsExempt;
|
|
251
|
+
|
|
252
|
+
IRateLimiter.SetRateLimitAddressExemptionParam[]
|
|
253
|
+
memory exemptions = new IRateLimiter.SetRateLimitAddressExemptionParam[](1);
|
|
254
|
+
exemptions[0] = IRateLimiter.SetRateLimitAddressExemptionParam({ user: EXEMPT_USER, isExempt: newExempt });
|
|
255
|
+
limiter.setRateLimitAddressExemptions(exemptions);
|
|
256
|
+
|
|
257
|
+
ghost_exemptUserIsExempt = newExempt;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function toggleGlobalDisable() public {
|
|
261
|
+
bool newDisabled = !ghost_globallyDisabled;
|
|
262
|
+
|
|
263
|
+
IRateLimiter.RateLimitGlobalConfig memory gc = limiter.getRateLimitGlobalConfig();
|
|
264
|
+
gc.isGloballyDisabled = newDisabled;
|
|
265
|
+
limiter.setRateLimitGlobalConfig(gc);
|
|
266
|
+
|
|
267
|
+
ghost_globallyDisabled = newDisabled;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function setState(uint96 outUsage, uint96 inUsage, uint32 ageSeconds) public {
|
|
271
|
+
outUsage = uint96(bound(outUsage, 0, currentOutboundLimit * 2));
|
|
272
|
+
inUsage = uint96(bound(inUsage, 0, currentInboundLimit * 2));
|
|
273
|
+
uint32 maxWindow = currentOutboundWindow > currentInboundWindow ? currentOutboundWindow : currentInboundWindow;
|
|
274
|
+
uint256 maxAge = block.timestamp < maxWindow ? block.timestamp : maxWindow;
|
|
275
|
+
ageSeconds = uint32(bound(ageSeconds, 0, maxAge));
|
|
276
|
+
|
|
277
|
+
uint40 lastUpdated = uint40(block.timestamp) - uint40(ageSeconds);
|
|
278
|
+
|
|
279
|
+
IRateLimiter.SetRateLimitStateParam[] memory params = new IRateLimiter.SetRateLimitStateParam[](1);
|
|
280
|
+
params[0] = IRateLimiter.SetRateLimitStateParam({
|
|
281
|
+
id: ID,
|
|
282
|
+
state: IRateLimiter.RateLimitState({
|
|
283
|
+
outboundUsage: outUsage,
|
|
284
|
+
inboundUsage: inUsage,
|
|
285
|
+
lastUpdated: lastUpdated
|
|
286
|
+
})
|
|
287
|
+
});
|
|
288
|
+
limiter.setRateLimitStates(params);
|
|
289
|
+
|
|
290
|
+
ghost_outboundUsage = outUsage;
|
|
291
|
+
ghost_inboundUsage = inUsage;
|
|
292
|
+
ghost_lastUpdated = lastUpdated;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
contract RateLimiterRBACUpgradeableInvariantTest is StdInvariant, Test {
|
|
297
|
+
RateLimiterRBACUpgradeableFuzzMock limiter;
|
|
298
|
+
RateLimiterRBACUpgradeableHandler handler;
|
|
299
|
+
uint256 constant PRISTINE_ID = uint256(keccak256("PRISTINE_ID"));
|
|
300
|
+
|
|
301
|
+
function setUp() public virtual {
|
|
302
|
+
limiter = _createRateLimiter(0, _globalStateEnabled());
|
|
303
|
+
handler = new RateLimiterRBACUpgradeableHandler(limiter, _handlerId());
|
|
304
|
+
|
|
305
|
+
limiter.grantRole(limiter.RATE_LIMITER_MANAGER_ROLE(), address(handler));
|
|
306
|
+
|
|
307
|
+
targetContract(address(handler));
|
|
308
|
+
|
|
309
|
+
bytes4[] memory selectors = new bytes4[](7);
|
|
310
|
+
selectors[0] = RateLimiterRBACUpgradeableHandler.outflow.selector;
|
|
311
|
+
selectors[1] = RateLimiterRBACUpgradeableHandler.inflow.selector;
|
|
312
|
+
selectors[2] = RateLimiterRBACUpgradeableHandler.warp.selector;
|
|
313
|
+
selectors[3] = RateLimiterRBACUpgradeableHandler.setConfig.selector;
|
|
314
|
+
selectors[4] = RateLimiterRBACUpgradeableHandler.toggleExemption.selector;
|
|
315
|
+
selectors[5] = RateLimiterRBACUpgradeableHandler.toggleGlobalDisable.selector;
|
|
316
|
+
selectors[6] = RateLimiterRBACUpgradeableHandler.setState.selector;
|
|
317
|
+
targetSelector(FuzzSelector({ addr: address(handler), selectors: selectors }));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function _createRateLimiter(
|
|
321
|
+
uint8 _scaleDecimals,
|
|
322
|
+
bool _useGlobalState
|
|
323
|
+
) internal virtual returns (RateLimiterRBACUpgradeableFuzzMock) {
|
|
324
|
+
RateLimiterRBACUpgradeableFuzzMock impl = new RateLimiterRBACUpgradeableFuzzMock(_scaleDecimals);
|
|
325
|
+
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
|
|
326
|
+
address(impl),
|
|
327
|
+
address(this),
|
|
328
|
+
abi.encodeWithSelector(
|
|
329
|
+
RateLimiterRBACUpgradeableFuzzMock.initialize.selector,
|
|
330
|
+
_useGlobalState,
|
|
331
|
+
address(this)
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
return RateLimiterRBACUpgradeableFuzzMock(address(proxy));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function _globalStateEnabled() internal pure virtual returns (bool) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function _handlerId() internal pure virtual returns (uint256) {
|
|
342
|
+
return uint256(keccak256("ID"));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// @notice Available should always be saturatingSub(limit, usage); max when globally disabled.
|
|
346
|
+
function invariant_AvailableConsistent() public view {
|
|
347
|
+
(uint256 outUsage, uint256 outAvail, uint256 inUsage, uint256 inAvail) = limiter.getRateLimitUsages(
|
|
348
|
+
handler.ID()
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (handler.ghost_globallyDisabled()) {
|
|
352
|
+
assertEq(outAvail, type(uint256).max, "Outbound available should be max when globally disabled");
|
|
353
|
+
assertEq(inAvail, type(uint256).max, "Inbound available should be max when globally disabled");
|
|
354
|
+
} else {
|
|
355
|
+
uint256 outLimit = handler.currentOutboundLimit();
|
|
356
|
+
uint256 inLimit = handler.currentInboundLimit();
|
|
357
|
+
|
|
358
|
+
assertEq(outAvail, (outLimit > outUsage) ? outLimit - outUsage : 0, "Outbound available mismatch");
|
|
359
|
+
assertEq(inAvail, (inLimit > inUsage) ? inLimit - inUsage : 0, "Inbound available mismatch");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// @notice On-chain config and global flags must match handler-tracked state.
|
|
364
|
+
function invariant_ConfigConsistency() public view {
|
|
365
|
+
IRateLimiter.RateLimit memory rl = limiter.rateLimits(handler.ID());
|
|
366
|
+
|
|
367
|
+
assertEq(rl.outboundLimit, handler.currentOutboundLimit(), "Outbound limit mismatch");
|
|
368
|
+
assertEq(rl.inboundLimit, handler.currentInboundLimit(), "Inbound limit mismatch");
|
|
369
|
+
assertEq(rl.outboundWindow, handler.currentOutboundWindow(), "Outbound window mismatch");
|
|
370
|
+
assertEq(rl.inboundWindow, handler.currentInboundWindow(), "Inbound window mismatch");
|
|
371
|
+
|
|
372
|
+
IRateLimiter.RateLimitGlobalConfig memory gc = limiter.getRateLimitGlobalConfig();
|
|
373
|
+
assertEq(gc.isGloballyDisabled, handler.ghost_globallyDisabled(), "Global disabled flag mismatch");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// @notice Contract-reported usage must match independent ghost decay calculation.
|
|
377
|
+
function invariant_GhostConsistency() public view {
|
|
378
|
+
(uint256 outUsage, , uint256 inUsage, ) = limiter.getRateLimitUsages(handler.ID());
|
|
379
|
+
(uint256 expectedOut, uint256 expectedIn) = handler.getGhostUsages();
|
|
380
|
+
|
|
381
|
+
assertEq(outUsage, expectedOut, "Ghost outbound usage mismatch");
|
|
382
|
+
assertEq(inUsage, expectedIn, "Ghost inbound usage mismatch");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// @notice Every outflow/inflow must succeed or revert exactly as predicted.
|
|
386
|
+
function invariant_RevertConsistency() public view {
|
|
387
|
+
assertFalse(handler.ghost_revertMismatch(), "Revert behavior diverged from predicted");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// @notice On-chain exemption mapping must match handler-tracked ghost.
|
|
391
|
+
function invariant_ExemptionConsistency() public view {
|
|
392
|
+
assertEq(
|
|
393
|
+
limiter.isRateLimitAddressExempt(handler.EXEMPT_USER()),
|
|
394
|
+
handler.ghost_exemptUserIsExempt(),
|
|
395
|
+
"Exemption state mismatch"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/// @notice lastUpdated must never exceed block.timestamp.
|
|
400
|
+
function invariant_LastUpdatedNotFuture() public view {
|
|
401
|
+
IRateLimiter.RateLimit memory rl = limiter.rateLimits(handler.ID());
|
|
402
|
+
assertTrue(rl.lastUpdated <= block.timestamp, "lastUpdated exceeds current block.timestamp");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// @notice Config bitmap must decode to the same flags the handler tracks.
|
|
406
|
+
function invariant_ConfigBitmapConsistency() public view {
|
|
407
|
+
IRateLimiter.RateLimit memory rl = limiter.rateLimits(handler.ID());
|
|
408
|
+
uint256 bitmap = rl.configBitmap;
|
|
409
|
+
|
|
410
|
+
assertTrue(bitmap & 1 != 0, "Override default config bit not set");
|
|
411
|
+
assertTrue(bitmap & 2 != 0, "Outbound enabled bit not set");
|
|
412
|
+
assertTrue(bitmap & 4 != 0, "Inbound enabled bit not set");
|
|
413
|
+
|
|
414
|
+
bool onChainNet = bitmap & 8 != 0;
|
|
415
|
+
bool onChainExemption = bitmap & 16 != 0;
|
|
416
|
+
assertEq(onChainNet, handler.netAccounting(), "Net accounting bitmap mismatch");
|
|
417
|
+
assertEq(onChainExemption, handler.addressExemptionEnabled(), "Address exemption bitmap mismatch");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/// @notice An untouched ID must always have zero usage (catches cross-ID state contamination).
|
|
421
|
+
function invariant_PristineIdUntouched() public view virtual {
|
|
422
|
+
(uint256 outUsage, , uint256 inUsage, ) = limiter.getRateLimitUsages(PRISTINE_ID);
|
|
423
|
+
assertEq(outUsage, 0, "Pristine ID has unexpected outbound usage");
|
|
424
|
+
assertEq(inUsage, 0, "Pristine ID has unexpected inbound usage");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
contract RateLimiterRBACUpgradeableInvariantGlobalStateTest is RateLimiterRBACUpgradeableInvariantTest {
|
|
429
|
+
function _globalStateEnabled() internal pure override returns (bool) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function _handlerId() internal pure override returns (uint256) {
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// @notice In global-state mode, all IDs share state so pristine ID mirrors active ID.
|
|
438
|
+
function invariant_PristineIdUntouched() public view override {
|
|
439
|
+
(uint256 outUsage, , uint256 inUsage, ) = limiter.getRateLimitUsages(PRISTINE_ID);
|
|
440
|
+
(uint256 activeOut, , uint256 activeIn, ) = limiter.getRateLimitUsages(handler.ID());
|
|
441
|
+
assertEq(outUsage, activeOut, "Global state: pristine ID should mirror active ID outbound usage");
|
|
442
|
+
assertEq(inUsage, activeIn, "Global state: pristine ID should mirror active ID inbound usage");
|
|
443
|
+
}
|
|
444
|
+
}
|