@layerzerolabs/oft-mint-burn-starknet 0.2.9

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 (98) hide show
  1. package/Scarb.lock +190 -0
  2. package/Scarb.toml +3 -0
  3. package/contracts/oft_mint_burn/Scarb.toml +29 -0
  4. package/contracts/oft_mint_burn/src/constants.cairo +11 -0
  5. package/contracts/oft_mint_burn/src/erc20_mint_burn_upgradeable/constants.cairo +17 -0
  6. package/contracts/oft_mint_burn/src/erc20_mint_burn_upgradeable/erc20_mint_burn_upgradeable.cairo +309 -0
  7. package/contracts/oft_mint_burn/src/erc20_mint_burn_upgradeable/interface.cairo +86 -0
  8. package/contracts/oft_mint_burn/src/errors.cairo +39 -0
  9. package/contracts/oft_mint_burn/src/interface.cairo +95 -0
  10. package/contracts/oft_mint_burn/src/lib.cairo +10 -0
  11. package/contracts/oft_mint_burn/src/oft_mint_burn_adapter.cairo +382 -0
  12. package/contracts/oft_mint_burn/tests/fuzzable/contract_address.cairo +21 -0
  13. package/contracts/oft_mint_burn/tests/lib.cairo +8 -0
  14. package/contracts/oft_mint_burn/tests/test_erc20_mint_burn_upgradeable.cairo +621 -0
  15. package/contracts/oft_mint_burn/tests/test_mint_burn_adapter.cairo +194 -0
  16. package/contracts/oft_mint_burn/tests/test_mint_burn_adapter_advanced.cairo +746 -0
  17. package/contracts/oft_mint_burn/tests/utils.cairo +84 -0
  18. package/dist/3UUTAAI4.js +9 -0
  19. package/dist/3UUTAAI4.js.map +1 -0
  20. package/dist/4EGWSIX7.js +18 -0
  21. package/dist/4EGWSIX7.js.map +1 -0
  22. package/dist/54KHZH3Y.cjs +22 -0
  23. package/dist/54KHZH3Y.cjs.map +1 -0
  24. package/dist/5KFUKPR2.js +1033 -0
  25. package/dist/5KFUKPR2.js.map +1 -0
  26. package/dist/5R6WMZVR.cjs +22 -0
  27. package/dist/5R6WMZVR.cjs.map +1 -0
  28. package/dist/CCHLUK5E.cjs +1035 -0
  29. package/dist/CCHLUK5E.cjs.map +1 -0
  30. package/dist/CGU4EJTF.cjs +14 -0
  31. package/dist/CGU4EJTF.cjs.map +1 -0
  32. package/dist/DJIRVXJ3.cjs +1914 -0
  33. package/dist/DJIRVXJ3.cjs.map +1 -0
  34. package/dist/E63KEOR5.cjs +11 -0
  35. package/dist/E63KEOR5.cjs.map +1 -0
  36. package/dist/FL52ASKH.cjs +16 -0
  37. package/dist/FL52ASKH.cjs.map +1 -0
  38. package/dist/GZXOKWQY.js +1303 -0
  39. package/dist/GZXOKWQY.js.map +1 -0
  40. package/dist/IQR2DIUY.cjs +1305 -0
  41. package/dist/IQR2DIUY.cjs.map +1 -0
  42. package/dist/MNNO3GDF.js +14 -0
  43. package/dist/MNNO3GDF.js.map +1 -0
  44. package/dist/NZRVZWHQ.js +1912 -0
  45. package/dist/NZRVZWHQ.js.map +1 -0
  46. package/dist/QYG4SI7W.js +18 -0
  47. package/dist/QYG4SI7W.js.map +1 -0
  48. package/dist/UE6XWQTX.js +12 -0
  49. package/dist/UE6XWQTX.js.map +1 -0
  50. package/dist/generated/abi/e-r-c20-mint-burn-upgradeable.cjs +13 -0
  51. package/dist/generated/abi/e-r-c20-mint-burn-upgradeable.cjs.map +1 -0
  52. package/dist/generated/abi/e-r-c20-mint-burn-upgradeable.d.ts +760 -0
  53. package/dist/generated/abi/e-r-c20-mint-burn-upgradeable.d.ts.map +1 -0
  54. package/dist/generated/abi/e-r-c20-mint-burn-upgradeable.js +4 -0
  55. package/dist/generated/abi/e-r-c20-mint-burn-upgradeable.js.map +1 -0
  56. package/dist/generated/abi/o-f-t-mint-burn-adapter.cjs +13 -0
  57. package/dist/generated/abi/o-f-t-mint-burn-adapter.cjs.map +1 -0
  58. package/dist/generated/abi/o-f-t-mint-burn-adapter.d.ts +1408 -0
  59. package/dist/generated/abi/o-f-t-mint-burn-adapter.d.ts.map +1 -0
  60. package/dist/generated/abi/o-f-t-mint-burn-adapter.js +4 -0
  61. package/dist/generated/abi/o-f-t-mint-burn-adapter.js.map +1 -0
  62. package/dist/generated/abi.cjs +19 -0
  63. package/dist/generated/abi.cjs.map +1 -0
  64. package/dist/generated/abi.d.ts +3 -0
  65. package/dist/generated/abi.d.ts.map +1 -0
  66. package/dist/generated/abi.js +6 -0
  67. package/dist/generated/abi.js.map +1 -0
  68. package/dist/generated/casm.cjs +17 -0
  69. package/dist/generated/casm.cjs.map +1 -0
  70. package/dist/generated/casm.d.ts +3 -0
  71. package/dist/generated/casm.d.ts.map +1 -0
  72. package/dist/generated/casm.js +4 -0
  73. package/dist/generated/casm.js.map +1 -0
  74. package/dist/generated/sierra.cjs +17 -0
  75. package/dist/generated/sierra.cjs.map +1 -0
  76. package/dist/generated/sierra.d.ts +3 -0
  77. package/dist/generated/sierra.d.ts.map +1 -0
  78. package/dist/generated/sierra.js +4 -0
  79. package/dist/generated/sierra.js.map +1 -0
  80. package/dist/generated/verification/index.cjs +14 -0
  81. package/dist/generated/verification/index.cjs.map +1 -0
  82. package/dist/generated/verification/index.d.ts +2 -0
  83. package/dist/generated/verification/index.d.ts.map +1 -0
  84. package/dist/generated/verification/index.js +5 -0
  85. package/dist/generated/verification/index.js.map +1 -0
  86. package/dist/generated/verification/oft_mint_burn.cjs +10 -0
  87. package/dist/generated/verification/oft_mint_burn.cjs.map +1 -0
  88. package/dist/generated/verification/oft_mint_burn.d.ts +4 -0
  89. package/dist/generated/verification/oft_mint_burn.d.ts.map +1 -0
  90. package/dist/generated/verification/oft_mint_burn.js +4 -0
  91. package/dist/generated/verification/oft_mint_burn.js.map +1 -0
  92. package/dist/index.cjs +31 -0
  93. package/dist/index.cjs.map +1 -0
  94. package/dist/index.d.ts +5 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.js +10 -0
  97. package/dist/index.js.map +1 -0
  98. package/package.json +53 -0
@@ -0,0 +1,746 @@
1
+ //! Advanced tests for OFTMintBurnAdapter
2
+ //! Tests for pausable, access control, rate limiting, fees, and upgrades
3
+
4
+ use layerzero::common::structs::packet::Origin;
5
+ use layerzero::endpoint::interfaces::layerzero_receiver::{
6
+ ILayerZeroReceiverDispatcher, ILayerZeroReceiverDispatcherTrait,
7
+ };
8
+ use layerzero::oapps::common::fee::interface::{IFeeDispatcher, IFeeDispatcherTrait};
9
+ use layerzero::oapps::common::rate_limiter::interface::{
10
+ IRateLimiterDispatcher, IRateLimiterDispatcherTrait,
11
+ };
12
+ use layerzero::oapps::common::rate_limiter::structs::{
13
+ RateLimitConfig, RateLimitDirection, RateLimitEnabled,
14
+ };
15
+ use layerzero::oapps::oapp::interface::{IOAppDispatcher, IOAppDispatcherTrait};
16
+ use layerzero::oapps::oft::interface::{IOFTDispatcher, IOFTDispatcherTrait};
17
+ use layerzero::oapps::oft::oft_msg_codec::OFTMsgCodec;
18
+ use layerzero::oapps::oft::structs::SendParam;
19
+ use layerzero::{MessageReceipt, MessagingFee};
20
+ use lz_utils::bytes::{Bytes32, ContractAddressIntoBytes32};
21
+ use oft_mint_burn::constants::{
22
+ FEE_MANAGER_ROLE, PAUSE_MANAGER_ROLE, RATE_LIMITER_MANAGER_ROLE, UPGRADE_MANAGER_ROLE,
23
+ };
24
+ use oft_mint_burn::errors::err_caller_not_owner_or_missing_role;
25
+ use oft_mint_burn::interface::{
26
+ IMintableTokenDispatcher, IMintableTokenDispatcherTrait, IOFTMintBurnAdapterDispatcher,
27
+ IOFTMintBurnAdapterDispatcherTrait, IOFTMintBurnAdapterSafeDispatcher,
28
+ IOFTMintBurnAdapterSafeDispatcherTrait,
29
+ };
30
+ use openzeppelin::access::accesscontrol::interface::{
31
+ IAccessControlDispatcher, IAccessControlDispatcherTrait,
32
+ };
33
+ use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
34
+ use snforge_std::{DeclareResultTrait, declare, mock_call, start_cheat_caller_address};
35
+ use starknet::{ContractAddress, SyscallResultTrait};
36
+ use starkware_utils_testing::test_utils::{assert_panic_with_error, cheat_caller_address_once};
37
+ use crate::utils::{
38
+ DIFF_DECIMALS, LZ_ENDPOINT, OFTMintBurnAdapterDeploy, OFT_OWNER, setup_mint_burn_adapter,
39
+ };
40
+
41
+ const UNAUTHORIZED_USER: ContractAddress = 'unauthorized_user'.try_into().unwrap();
42
+ const FEE_MANAGER: ContractAddress = 'fee_manager'.try_into().unwrap();
43
+ const RATE_LIMITER_MANAGER: ContractAddress = 'rate_limiter_manager'.try_into().unwrap();
44
+ const UPGRADE_MANAGER: ContractAddress = 'upgrade_manager'.try_into().unwrap();
45
+ const PAUSE_MANAGER: ContractAddress = 'pause_manager'.try_into().unwrap();
46
+
47
+ // =============================== Pausable Tests ===============================
48
+
49
+ #[test]
50
+ fn test_pause_and_unpause() {
51
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
52
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
53
+
54
+ // Owner can pause
55
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
56
+ adapter.pause();
57
+
58
+ // Owner can unpause
59
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
60
+ adapter.unpause();
61
+ // If we got here without reverting, pause/unpause works
62
+ }
63
+
64
+ #[test]
65
+ #[feature("safe_dispatcher")]
66
+ fn test_pause_unauthorized() {
67
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
68
+ let adapter = IOFTMintBurnAdapterSafeDispatcher { contract_address: oft_mint_burn_adapter };
69
+
70
+ // Unauthorized user cannot pause (needs owner or PAUSE_MANAGER_ROLE)
71
+ cheat_caller_address_once(oft_mint_burn_adapter, UNAUTHORIZED_USER);
72
+ let result = adapter.pause();
73
+ assert_panic_with_error(result, err_caller_not_owner_or_missing_role(PAUSE_MANAGER_ROLE));
74
+ }
75
+
76
+ #[test]
77
+ fn test_grant_pause_manager_role() {
78
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
79
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
80
+
81
+ // Owner can grant PAUSE_MANAGER_ROLE
82
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
83
+ access_control.grant_role(PAUSE_MANAGER_ROLE, PAUSE_MANAGER);
84
+ assert!(access_control.has_role(PAUSE_MANAGER_ROLE, PAUSE_MANAGER));
85
+ }
86
+
87
+ #[test]
88
+ fn test_pause_manager_can_pause_and_unpause() {
89
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
90
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
91
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
92
+
93
+ // Grant PAUSE_MANAGER_ROLE to PAUSE_MANAGER
94
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
95
+ access_control.grant_role(PAUSE_MANAGER_ROLE, PAUSE_MANAGER);
96
+
97
+ // Pause manager can pause
98
+ cheat_caller_address_once(oft_mint_burn_adapter, PAUSE_MANAGER);
99
+ adapter.pause();
100
+
101
+ // Pause manager can unpause
102
+ cheat_caller_address_once(oft_mint_burn_adapter, PAUSE_MANAGER);
103
+ adapter.unpause();
104
+ }
105
+
106
+ #[test]
107
+ #[feature("safe_dispatcher")]
108
+ fn test_unpause_unauthorized() {
109
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
110
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
111
+ let safe_adapter = IOFTMintBurnAdapterSafeDispatcher {
112
+ contract_address: oft_mint_burn_adapter,
113
+ };
114
+
115
+ // First pause with owner
116
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
117
+ adapter.pause();
118
+
119
+ // Unauthorized user cannot unpause (needs owner or PAUSE_MANAGER_ROLE)
120
+ cheat_caller_address_once(oft_mint_burn_adapter, UNAUTHORIZED_USER);
121
+ let result = safe_adapter.unpause();
122
+ assert_panic_with_error(result, err_caller_not_owner_or_missing_role(PAUSE_MANAGER_ROLE));
123
+ }
124
+
125
+ #[test]
126
+ #[should_panic(expected: 'Pausable: paused')]
127
+ fn test_send_when_paused() {
128
+ let OFTMintBurnAdapterDeploy {
129
+ oft_mint_burn_adapter, erc20_token, ..,
130
+ } = setup_mint_burn_adapter();
131
+
132
+ let dst_eid = 1_u32;
133
+ let peer = 0x123_u256;
134
+ let amount_ld = 1000_u256 * DIFF_DECIMALS;
135
+
136
+ // Setup peer
137
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
138
+ IOAppDispatcher { contract_address: oft_mint_burn_adapter }.set_peer(dst_eid, peer.into());
139
+
140
+ // Pause contract
141
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
142
+ IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter }.pause();
143
+
144
+ // Try to send - should fail
145
+ let user: ContractAddress = 'user'.try_into().unwrap();
146
+ let send_param = SendParam {
147
+ dst_eid,
148
+ to: peer.into(),
149
+ amount_ld,
150
+ min_amount_ld: amount_ld,
151
+ extra_options: Default::default(),
152
+ compose_msg: Default::default(),
153
+ oft_cmd: Default::default(),
154
+ };
155
+
156
+ // Mint tokens to user
157
+ cheat_caller_address_once(erc20_token, oft_mint_burn_adapter);
158
+ IMintableTokenDispatcher { contract_address: erc20_token }.permissioned_mint(user, amount_ld);
159
+
160
+ // Mock endpoint calls
161
+ mock_call(LZ_ENDPOINT, selector!("quote"), MessagingFee { native_fee: 0, lz_token_fee: 0 }, 1);
162
+ mock_call(
163
+ LZ_ENDPOINT,
164
+ selector!("send"),
165
+ MessageReceipt { guid: Bytes32 { value: 1 }, nonce: 1, payees: array![] },
166
+ 1,
167
+ );
168
+
169
+ // This should panic with "Pausable: paused"
170
+ cheat_caller_address_once(oft_mint_burn_adapter, user);
171
+ IOFTDispatcher { contract_address: oft_mint_burn_adapter }
172
+ .send(send_param, MessagingFee { native_fee: 0, lz_token_fee: 0 }, user);
173
+ }
174
+
175
+ #[test]
176
+ #[should_panic(expected: 'Pausable: paused')]
177
+ fn test_receive_when_paused() {
178
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
179
+
180
+ let src_eid = 1_u32;
181
+ let sender = 0x123_u256;
182
+ let receiver: ContractAddress = 'receiver'.try_into().unwrap();
183
+ let amount_sd = 1000_u64;
184
+
185
+ // Setup peer
186
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
187
+ IOAppDispatcher { contract_address: oft_mint_burn_adapter }.set_peer(src_eid, sender.into());
188
+
189
+ // Pause contract
190
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
191
+ IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter }.pause();
192
+
193
+ // Try to receive - should fail
194
+ let receiver_bytes32: Bytes32 = receiver.into();
195
+ let (message, _) = OFTMsgCodec::encode(receiver_bytes32, amount_sd, @"");
196
+ let origin = Origin { src_eid, sender: sender.into(), nonce: 1 };
197
+
198
+ // This should panic with "Pausable: paused"
199
+ start_cheat_caller_address(oft_mint_burn_adapter, LZ_ENDPOINT);
200
+ ILayerZeroReceiverDispatcher { contract_address: oft_mint_burn_adapter }
201
+ .lz_receive(origin, Bytes32 { value: 1 }, message, receiver, Default::default(), 0);
202
+ }
203
+
204
+ // =============================== Access Control Tests ===============================
205
+
206
+ #[test]
207
+ fn test_grant_and_revoke_fee_manager_role() {
208
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
209
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
210
+
211
+ // Owner can grant FEE_MANAGER_ROLE
212
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
213
+ access_control.grant_role(FEE_MANAGER_ROLE, FEE_MANAGER);
214
+ assert!(access_control.has_role(FEE_MANAGER_ROLE, FEE_MANAGER));
215
+
216
+ // Owner can revoke FEE_MANAGER_ROLE
217
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
218
+ access_control.revoke_role(FEE_MANAGER_ROLE, FEE_MANAGER);
219
+ assert!(!access_control.has_role(FEE_MANAGER_ROLE, FEE_MANAGER));
220
+ }
221
+
222
+ #[test]
223
+ fn test_grant_rate_limiter_manager_role() {
224
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
225
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
226
+
227
+ // Owner can grant RATE_LIMITER_MANAGER_ROLE
228
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
229
+ access_control.grant_role(RATE_LIMITER_MANAGER_ROLE, RATE_LIMITER_MANAGER);
230
+ assert!(access_control.has_role(RATE_LIMITER_MANAGER_ROLE, RATE_LIMITER_MANAGER));
231
+ }
232
+
233
+ #[test]
234
+ fn test_grant_upgrade_manager_role() {
235
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
236
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
237
+
238
+ // Owner can grant UPGRADE_MANAGER_ROLE
239
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
240
+ access_control.grant_role(UPGRADE_MANAGER_ROLE, UPGRADE_MANAGER);
241
+ assert!(access_control.has_role(UPGRADE_MANAGER_ROLE, UPGRADE_MANAGER));
242
+ }
243
+
244
+ #[test]
245
+ fn test_rate_limiter_manager_can_set_rate_limits() {
246
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
247
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
248
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
249
+
250
+ // Grant RATE_LIMITER_MANAGER_ROLE
251
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
252
+ access_control.grant_role(RATE_LIMITER_MANAGER_ROLE, RATE_LIMITER_MANAGER);
253
+
254
+ // Rate limiter manager can set rate limits
255
+ let dst_eid = 1_u32;
256
+ let rate_limits = array![
257
+ RateLimitConfig { dst_eid, limit: 1000000, window: 3600 } // 1M per hour
258
+ ];
259
+
260
+ cheat_caller_address_once(oft_mint_burn_adapter, RATE_LIMITER_MANAGER);
261
+ adapter.set_rate_limits(rate_limits, RateLimitDirection::Outbound);
262
+ }
263
+
264
+ #[test]
265
+ fn test_owner_can_set_rate_limits_without_role() {
266
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
267
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
268
+
269
+ // Owner can set rate limits even without explicit role
270
+ let dst_eid = 1_u32;
271
+ let rate_limits = array![RateLimitConfig { dst_eid, limit: 1000000, window: 3600 }];
272
+
273
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
274
+ adapter.set_rate_limits(rate_limits, RateLimitDirection::Outbound);
275
+ }
276
+
277
+ #[test]
278
+ #[feature("safe_dispatcher")]
279
+ fn test_unauthorized_cannot_set_rate_limits() {
280
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
281
+ let adapter = IOFTMintBurnAdapterSafeDispatcher { contract_address: oft_mint_burn_adapter };
282
+
283
+ let dst_eid = 1_u32;
284
+ let rate_limits = array![RateLimitConfig { dst_eid, limit: 1000000, window: 3600 }];
285
+
286
+ // Unauthorized user cannot set rate limits
287
+ cheat_caller_address_once(oft_mint_burn_adapter, UNAUTHORIZED_USER);
288
+ let result = adapter.set_rate_limits(rate_limits, RateLimitDirection::Outbound);
289
+ assert_panic_with_error(
290
+ result, err_caller_not_owner_or_missing_role(RATE_LIMITER_MANAGER_ROLE),
291
+ );
292
+ }
293
+
294
+ // =============================== Fee Management Tests ===============================
295
+
296
+ #[test]
297
+ fn test_fee_balance_initially_zero() {
298
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
299
+
300
+ // Check initial fee balance
301
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
302
+ assert_eq!(adapter.fee_balance(), 0);
303
+ }
304
+
305
+ #[test]
306
+ #[should_panic]
307
+ fn test_withdraw_fees_when_zero() {
308
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
309
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
310
+
311
+ let recipient: ContractAddress = 'recipient'.try_into().unwrap();
312
+
313
+ // Should fail because fee_balance is 0
314
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
315
+ adapter.withdraw_fees(recipient);
316
+ }
317
+
318
+ #[test]
319
+ #[feature("safe_dispatcher")]
320
+ fn test_unauthorized_cannot_withdraw_fees() {
321
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
322
+ let adapter = IOFTMintBurnAdapterSafeDispatcher { contract_address: oft_mint_burn_adapter };
323
+
324
+ let recipient: ContractAddress = 'recipient'.try_into().unwrap();
325
+
326
+ // Unauthorized user cannot withdraw fees
327
+ cheat_caller_address_once(oft_mint_burn_adapter, UNAUTHORIZED_USER);
328
+ let result = adapter.withdraw_fees(recipient);
329
+ assert_panic_with_error(result, err_caller_not_owner_or_missing_role(FEE_MANAGER_ROLE));
330
+ }
331
+
332
+ #[test]
333
+ fn test_fee_manager_role_grant() {
334
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
335
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
336
+
337
+ // Grant FEE_MANAGER_ROLE
338
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
339
+ access_control.grant_role(FEE_MANAGER_ROLE, FEE_MANAGER);
340
+
341
+ // Verify role was granted
342
+ assert!(access_control.has_role(FEE_MANAGER_ROLE, FEE_MANAGER));
343
+ }
344
+
345
+ #[test]
346
+ fn test_withdraw_fees_successfully() {
347
+ let OFTMintBurnAdapterDeploy {
348
+ oft_mint_burn_adapter, erc20_token, ..,
349
+ } = setup_mint_burn_adapter();
350
+
351
+ let dst_eid = 1_u32;
352
+ let peer = 0x123_u256;
353
+ let user: ContractAddress = 'user'.try_into().unwrap();
354
+ let fee_recipient: ContractAddress = 'fee_recipient'.try_into().unwrap();
355
+ let amount_ld = 10000_u256 * DIFF_DECIMALS; // 10000 tokens
356
+
357
+ // Setup peer
358
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
359
+ IOAppDispatcher { contract_address: oft_mint_burn_adapter }.set_peer(dst_eid, peer.into());
360
+
361
+ // Set fee BPS (1% = 100 bps)
362
+ let fee_dispatcher = IFeeDispatcher { contract_address: oft_mint_burn_adapter };
363
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
364
+ fee_dispatcher.set_fee_bps(dst_eid, 100, true); // 1% fee
365
+
366
+ // Mint tokens to user
367
+ cheat_caller_address_once(erc20_token, oft_mint_burn_adapter);
368
+ IMintableTokenDispatcher { contract_address: erc20_token }.permissioned_mint(user, amount_ld);
369
+
370
+ // Mock endpoint calls
371
+ mock_call(LZ_ENDPOINT, selector!("quote"), MessagingFee { native_fee: 0, lz_token_fee: 0 }, 1);
372
+ mock_call(
373
+ LZ_ENDPOINT,
374
+ selector!("send"),
375
+ MessageReceipt { guid: Bytes32 { value: 1 }, nonce: 1, payees: array![] },
376
+ 1,
377
+ );
378
+
379
+ // Send tokens (this should collect fees)
380
+ let send_param = SendParam {
381
+ dst_eid,
382
+ to: peer.into(),
383
+ amount_ld,
384
+ min_amount_ld: 0, // Allow slippage for fee
385
+ extra_options: Default::default(),
386
+ compose_msg: Default::default(),
387
+ oft_cmd: Default::default(),
388
+ };
389
+
390
+ cheat_caller_address_once(oft_mint_burn_adapter, user);
391
+ IOFTDispatcher { contract_address: oft_mint_burn_adapter }
392
+ .send(send_param, MessagingFee { native_fee: 0, lz_token_fee: 0 }, user);
393
+
394
+ // Check that fees were collected
395
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
396
+ let fee_balance = adapter.fee_balance();
397
+ assert!(fee_balance > 0, "Fee balance should be greater than 0");
398
+
399
+ // Check initial balance of fee recipient
400
+ let recipient_balance_before = IERC20Dispatcher { contract_address: erc20_token }
401
+ .balance_of(fee_recipient);
402
+ assert_eq!(recipient_balance_before, 0);
403
+
404
+ // Withdraw fees
405
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
406
+ adapter.withdraw_fees(fee_recipient);
407
+
408
+ // Check that fee balance is now 0
409
+ assert_eq!(adapter.fee_balance(), 0);
410
+
411
+ // Check that recipient received the fees
412
+ let recipient_balance_after = IERC20Dispatcher { contract_address: erc20_token }
413
+ .balance_of(fee_recipient);
414
+ assert_eq!(recipient_balance_after, fee_balance);
415
+ }
416
+
417
+ #[test]
418
+ fn test_fee_manager_can_withdraw_fees() {
419
+ let OFTMintBurnAdapterDeploy {
420
+ oft_mint_burn_adapter, erc20_token, ..,
421
+ } = setup_mint_burn_adapter();
422
+
423
+ let dst_eid = 1_u32;
424
+ let peer = 0x123_u256;
425
+ let user: ContractAddress = 'user'.try_into().unwrap();
426
+ let fee_recipient: ContractAddress = 'fee_recipient'.try_into().unwrap();
427
+ let amount_ld = 10000_u256 * DIFF_DECIMALS;
428
+
429
+ // Setup peer
430
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
431
+ IOAppDispatcher { contract_address: oft_mint_burn_adapter }.set_peer(dst_eid, peer.into());
432
+
433
+ // Set fee BPS (1% = 100 bps)
434
+ let fee_dispatcher = IFeeDispatcher { contract_address: oft_mint_burn_adapter };
435
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
436
+ fee_dispatcher.set_fee_bps(dst_eid, 100, true);
437
+
438
+ // Grant FEE_MANAGER_ROLE
439
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
440
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
441
+ access_control.grant_role(FEE_MANAGER_ROLE, FEE_MANAGER);
442
+
443
+ // Mint tokens to user
444
+ cheat_caller_address_once(erc20_token, oft_mint_burn_adapter);
445
+ IMintableTokenDispatcher { contract_address: erc20_token }.permissioned_mint(user, amount_ld);
446
+
447
+ // Mock endpoint calls
448
+ mock_call(LZ_ENDPOINT, selector!("quote"), MessagingFee { native_fee: 0, lz_token_fee: 0 }, 1);
449
+ mock_call(
450
+ LZ_ENDPOINT,
451
+ selector!("send"),
452
+ MessageReceipt { guid: Bytes32 { value: 1 }, nonce: 1, payees: array![] },
453
+ 1,
454
+ );
455
+
456
+ // Send tokens to collect fees
457
+ let send_param = SendParam {
458
+ dst_eid,
459
+ to: peer.into(),
460
+ amount_ld,
461
+ min_amount_ld: 0,
462
+ extra_options: Default::default(),
463
+ compose_msg: Default::default(),
464
+ oft_cmd: Default::default(),
465
+ };
466
+
467
+ cheat_caller_address_once(oft_mint_burn_adapter, user);
468
+ IOFTDispatcher { contract_address: oft_mint_burn_adapter }
469
+ .send(send_param, MessagingFee { native_fee: 0, lz_token_fee: 0 }, user);
470
+
471
+ // Fee manager can withdraw fees
472
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
473
+ let fee_balance = adapter.fee_balance();
474
+
475
+ cheat_caller_address_once(oft_mint_burn_adapter, FEE_MANAGER);
476
+ adapter.withdraw_fees(fee_recipient);
477
+
478
+ // Check that fee balance is now 0 and recipient received fees
479
+ assert_eq!(adapter.fee_balance(), 0);
480
+ let recipient_balance = IERC20Dispatcher { contract_address: erc20_token }
481
+ .balance_of(fee_recipient);
482
+ assert_eq!(recipient_balance, fee_balance);
483
+ }
484
+
485
+ // =============================== Rate Limiting Tests ===============================
486
+
487
+ #[test]
488
+ fn test_rate_limit_enable_and_disable() {
489
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
490
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
491
+ let rate_limiter = IRateLimiterDispatcher { contract_address: oft_mint_burn_adapter };
492
+
493
+ // Initially both should be disabled
494
+ let enabled = rate_limiter.get_rate_limit_enabled();
495
+ assert!(!enabled.is_outbound_enabled);
496
+ assert!(!enabled.is_inbound_enabled);
497
+
498
+ // Enable outbound rate limiting
499
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
500
+ adapter
501
+ .set_rate_limits_enabled(
502
+ RateLimitEnabled { is_outbound_enabled: true, is_inbound_enabled: false },
503
+ );
504
+
505
+ let enabled = rate_limiter.get_rate_limit_enabled();
506
+ assert!(enabled.is_outbound_enabled);
507
+ assert!(!enabled.is_inbound_enabled);
508
+
509
+ // Enable both
510
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
511
+ adapter
512
+ .set_rate_limits_enabled(
513
+ RateLimitEnabled { is_outbound_enabled: true, is_inbound_enabled: true },
514
+ );
515
+
516
+ let enabled = rate_limiter.get_rate_limit_enabled();
517
+ assert!(enabled.is_outbound_enabled);
518
+ assert!(enabled.is_inbound_enabled);
519
+ }
520
+
521
+ #[test]
522
+ #[feature("safe_dispatcher")]
523
+ fn test_unauthorized_cannot_enable_rate_limits() {
524
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
525
+ let adapter = IOFTMintBurnAdapterSafeDispatcher { contract_address: oft_mint_burn_adapter };
526
+
527
+ // Unauthorized user cannot enable rate limits
528
+ cheat_caller_address_once(oft_mint_burn_adapter, UNAUTHORIZED_USER);
529
+ let result = adapter
530
+ .set_rate_limits_enabled(
531
+ RateLimitEnabled { is_outbound_enabled: true, is_inbound_enabled: true },
532
+ );
533
+ assert_panic_with_error(
534
+ result, err_caller_not_owner_or_missing_role(RATE_LIMITER_MANAGER_ROLE),
535
+ );
536
+ }
537
+
538
+ #[test]
539
+ fn test_set_outbound_rate_limit() {
540
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
541
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
542
+ let rate_limiter = IRateLimiterDispatcher { contract_address: oft_mint_burn_adapter };
543
+
544
+ let dst_eid = 1_u32;
545
+ let limit = 1000000_u128;
546
+ let window = 3600_u64;
547
+
548
+ // Set outbound rate limit
549
+ let rate_limits = array![RateLimitConfig { dst_eid, limit, window }];
550
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
551
+ adapter.set_rate_limits(rate_limits, RateLimitDirection::Outbound);
552
+
553
+ // Check the rate limit was set
554
+ let rate_limit = rate_limiter.get_outbound_rate_limit(dst_eid);
555
+ assert_eq!(rate_limit.limit, limit);
556
+ assert_eq!(rate_limit.window, window);
557
+ assert_eq!(rate_limit.amount_in_flight, 0);
558
+ }
559
+
560
+ #[test]
561
+ fn test_set_inbound_rate_limit() {
562
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
563
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
564
+ let rate_limiter = IRateLimiterDispatcher { contract_address: oft_mint_burn_adapter };
565
+
566
+ let src_eid = 1_u32;
567
+ let limit = 500000_u128;
568
+ let window = 1800_u64;
569
+
570
+ // Set inbound rate limit
571
+ let rate_limits = array![RateLimitConfig { dst_eid: src_eid, limit, window }];
572
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
573
+ adapter.set_rate_limits(rate_limits, RateLimitDirection::Inbound);
574
+
575
+ // Check the rate limit was set
576
+ let rate_limit = rate_limiter.get_inbound_rate_limit(src_eid);
577
+ assert_eq!(rate_limit.limit, limit);
578
+ assert_eq!(rate_limit.window, window);
579
+ assert_eq!(rate_limit.amount_in_flight, 0);
580
+ }
581
+
582
+ // =============================== Upgrade Tests ===============================
583
+
584
+ #[test]
585
+ fn test_upgrade_manager_can_upgrade() {
586
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
587
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
588
+ let access_control = IAccessControlDispatcher { contract_address: oft_mint_burn_adapter };
589
+
590
+ // Grant UPGRADE_MANAGER_ROLE
591
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
592
+ access_control.grant_role(UPGRADE_MANAGER_ROLE, UPGRADE_MANAGER);
593
+
594
+ // Declare a new class (we'll use the same contract class for testing)
595
+ let new_class = declare("OFTMintBurnAdapter").unwrap_syscall();
596
+ let new_class_hash = *new_class.contract_class().class_hash;
597
+
598
+ // Upgrade manager can upgrade
599
+ cheat_caller_address_once(oft_mint_burn_adapter, UPGRADE_MANAGER);
600
+ adapter.upgrade(new_class_hash);
601
+ }
602
+
603
+ #[test]
604
+ fn test_owner_can_upgrade() {
605
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
606
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
607
+
608
+ // Declare a new class
609
+ let new_class = declare("OFTMintBurnAdapter").unwrap_syscall();
610
+ let new_class_hash = *new_class.contract_class().class_hash;
611
+
612
+ // Owner can upgrade even without explicit role
613
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
614
+ adapter.upgrade(new_class_hash);
615
+ }
616
+
617
+ #[test]
618
+ #[feature("safe_dispatcher")]
619
+ fn test_unauthorized_cannot_upgrade() {
620
+ let OFTMintBurnAdapterDeploy { oft_mint_burn_adapter, .. } = setup_mint_burn_adapter();
621
+ let adapter = IOFTMintBurnAdapterSafeDispatcher { contract_address: oft_mint_burn_adapter };
622
+
623
+ // Declare a new class
624
+ let new_class = declare("OFTMintBurnAdapter").unwrap_syscall();
625
+ let new_class_hash = *new_class.contract_class().class_hash;
626
+
627
+ // Unauthorized user cannot upgrade
628
+ cheat_caller_address_once(oft_mint_burn_adapter, UNAUTHORIZED_USER);
629
+ let result = adapter.upgrade(new_class_hash);
630
+ assert_panic_with_error(result, err_caller_not_owner_or_missing_role(UPGRADE_MANAGER_ROLE));
631
+ }
632
+
633
+ // =============================== Integration Tests ===============================
634
+
635
+ #[test]
636
+ fn test_send_with_pausable_and_rate_limit() {
637
+ let OFTMintBurnAdapterDeploy {
638
+ oft_mint_burn_adapter, erc20_token, ..,
639
+ } = setup_mint_burn_adapter();
640
+
641
+ let dst_eid = 1_u32;
642
+ let peer = 0x123_u256;
643
+ let user: ContractAddress = 'user'.try_into().unwrap();
644
+ let amount_ld = 100_u256 * DIFF_DECIMALS;
645
+
646
+ // Setup peer
647
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
648
+ IOAppDispatcher { contract_address: oft_mint_burn_adapter }.set_peer(dst_eid, peer.into());
649
+
650
+ // Set rate limit
651
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
652
+ let rate_limits = array![
653
+ RateLimitConfig {
654
+ dst_eid, limit: 1000_u128 * DIFF_DECIMALS.try_into().unwrap(), window: 3600,
655
+ },
656
+ ];
657
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
658
+ adapter.set_rate_limits(rate_limits, RateLimitDirection::Outbound);
659
+
660
+ // Enable outbound rate limiting
661
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
662
+ adapter
663
+ .set_rate_limits_enabled(
664
+ RateLimitEnabled { is_outbound_enabled: true, is_inbound_enabled: false },
665
+ );
666
+
667
+ // Mint tokens to user
668
+ cheat_caller_address_once(erc20_token, oft_mint_burn_adapter);
669
+ IMintableTokenDispatcher { contract_address: erc20_token }.permissioned_mint(user, amount_ld);
670
+
671
+ // Mock endpoint calls
672
+ mock_call(LZ_ENDPOINT, selector!("quote"), MessagingFee { native_fee: 0, lz_token_fee: 0 }, 1);
673
+ mock_call(
674
+ LZ_ENDPOINT,
675
+ selector!("send"),
676
+ MessageReceipt { guid: Bytes32 { value: 1 }, nonce: 1, payees: array![] },
677
+ 1,
678
+ );
679
+
680
+ // Send tokens (should succeed as it's under rate limit)
681
+ let send_param = SendParam {
682
+ dst_eid,
683
+ to: peer.into(),
684
+ amount_ld,
685
+ min_amount_ld: amount_ld,
686
+ extra_options: Default::default(),
687
+ compose_msg: Default::default(),
688
+ oft_cmd: Default::default(),
689
+ };
690
+
691
+ cheat_caller_address_once(oft_mint_burn_adapter, user);
692
+ IOFTDispatcher { contract_address: oft_mint_burn_adapter }
693
+ .send(send_param, MessagingFee { native_fee: 0, lz_token_fee: 0 }, user);
694
+
695
+ // Verify tokens were burned
696
+ let balance = IERC20Dispatcher { contract_address: erc20_token }.balance_of(user);
697
+ assert_eq!(balance, 0);
698
+ }
699
+
700
+ #[test]
701
+ fn test_receive_with_pausable_and_rate_limit() {
702
+ let OFTMintBurnAdapterDeploy {
703
+ oft_mint_burn_adapter, erc20_token, ..,
704
+ } = setup_mint_burn_adapter();
705
+
706
+ let src_eid = 1_u32;
707
+ let sender = 0x123_u256;
708
+ let receiver: ContractAddress = 'receiver'.try_into().unwrap();
709
+ let amount_sd = 100_u64;
710
+ let amount_ld = amount_sd.into() * DIFF_DECIMALS;
711
+
712
+ // Setup peer
713
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
714
+ IOAppDispatcher { contract_address: oft_mint_burn_adapter }.set_peer(src_eid, sender.into());
715
+
716
+ // Set inbound rate limit
717
+ let adapter = IOFTMintBurnAdapterDispatcher { contract_address: oft_mint_burn_adapter };
718
+ let rate_limits = array![
719
+ RateLimitConfig {
720
+ dst_eid: src_eid, limit: 1000_u128 * DIFF_DECIMALS.try_into().unwrap(), window: 3600,
721
+ },
722
+ ];
723
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
724
+ adapter.set_rate_limits(rate_limits, RateLimitDirection::Inbound);
725
+
726
+ // Enable inbound rate limiting
727
+ cheat_caller_address_once(oft_mint_burn_adapter, OFT_OWNER);
728
+ adapter
729
+ .set_rate_limits_enabled(
730
+ RateLimitEnabled { is_outbound_enabled: false, is_inbound_enabled: true },
731
+ );
732
+
733
+ // Receive tokens (should succeed as it's under rate limit)
734
+ let receiver_bytes32: Bytes32 = receiver.into();
735
+ let (message, _) = OFTMsgCodec::encode(receiver_bytes32, amount_sd, @"");
736
+ let origin = Origin { src_eid, sender: sender.into(), nonce: 1 };
737
+
738
+ start_cheat_caller_address(oft_mint_burn_adapter, LZ_ENDPOINT);
739
+ ILayerZeroReceiverDispatcher { contract_address: oft_mint_burn_adapter }
740
+ .lz_receive(origin, Bytes32 { value: 1 }, message, receiver, Default::default(), 0);
741
+
742
+ // Verify tokens were minted
743
+ let balance = IERC20Dispatcher { contract_address: erc20_token }.balance_of(receiver);
744
+ assert_eq!(balance, amount_ld);
745
+ }
746
+