create-kozalak-l1 0.1.0

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 (61) hide show
  1. package/README.md +87 -0
  2. package/dist/deploy.js +63 -0
  3. package/dist/forge.js +27 -0
  4. package/dist/index.js +176 -0
  5. package/dist/prompts.js +23 -0
  6. package/dist/scaffold.js +33 -0
  7. package/dist/templates.js +108 -0
  8. package/package.json +29 -0
  9. package/templates/erc20-gas/.env.example +19 -0
  10. package/templates/erc20-gas/.gitmodules +6 -0
  11. package/templates/erc20-gas/README.md +48 -0
  12. package/templates/erc20-gas/foundry.toml +35 -0
  13. package/templates/erc20-gas/gitignore +5 -0
  14. package/templates/erc20-gas/remappings.txt +2 -0
  15. package/templates/erc20-gas/script/DeployERC20Gas.s.sol +131 -0
  16. package/templates/erc20-gas/src/KozaGasToken.sol +105 -0
  17. package/templates/erc20-gas/test/DeployERC20Gas.t.sol +60 -0
  18. package/templates/erc20-gas/test/ERC20Gas.invariants.t.sol +144 -0
  19. package/templates/erc20-gas/test/ERC20Gas.t.sol +354 -0
  20. package/templates/erc721-collection/.env.example +37 -0
  21. package/templates/erc721-collection/.gitmodules +6 -0
  22. package/templates/erc721-collection/README.md +48 -0
  23. package/templates/erc721-collection/foundry.toml +35 -0
  24. package/templates/erc721-collection/gitignore +5 -0
  25. package/templates/erc721-collection/remappings.txt +2 -0
  26. package/templates/erc721-collection/script/DeployERC721Collection.s.sol +151 -0
  27. package/templates/erc721-collection/src/KozaCollection.sol +281 -0
  28. package/templates/erc721-collection/test/DeployERC721Collection.t.sol +76 -0
  29. package/templates/erc721-collection/test/ERC721Collection.invariants.t.sol +175 -0
  30. package/templates/erc721-collection/test/ERC721Collection.t.sol +501 -0
  31. package/templates/ictt-bridge/.env.example +19 -0
  32. package/templates/ictt-bridge/.gitmodules +9 -0
  33. package/templates/ictt-bridge/README.md +49 -0
  34. package/templates/ictt-bridge/foundry.toml +41 -0
  35. package/templates/ictt-bridge/gitignore +5 -0
  36. package/templates/ictt-bridge/remappings.txt +8 -0
  37. package/templates/ictt-bridge/script/DeployTokenHome.s.sol +139 -0
  38. package/templates/ictt-bridge/src/KozaTokenHome.sol +57 -0
  39. package/templates/ictt-bridge/src/KozaTokenRemote.sol +65 -0
  40. package/templates/ictt-bridge/test/ICTTBridge.t.sol +157 -0
  41. package/templates/soulbound-credential/.env.example +19 -0
  42. package/templates/soulbound-credential/.gitmodules +6 -0
  43. package/templates/soulbound-credential/README.md +48 -0
  44. package/templates/soulbound-credential/foundry.toml +35 -0
  45. package/templates/soulbound-credential/gitignore +5 -0
  46. package/templates/soulbound-credential/remappings.txt +2 -0
  47. package/templates/soulbound-credential/script/DeployCredential.s.sol +126 -0
  48. package/templates/soulbound-credential/src/KozaCredential.sol +201 -0
  49. package/templates/soulbound-credential/test/DeployCredential.t.sol +46 -0
  50. package/templates/soulbound-credential/test/Soulbound.invariants.t.sol +133 -0
  51. package/templates/soulbound-credential/test/Soulbound.t.sol +319 -0
  52. package/templates/treasury-multisig/.env.example +19 -0
  53. package/templates/treasury-multisig/.gitmodules +6 -0
  54. package/templates/treasury-multisig/README.md +48 -0
  55. package/templates/treasury-multisig/foundry.toml +35 -0
  56. package/templates/treasury-multisig/gitignore +5 -0
  57. package/templates/treasury-multisig/remappings.txt +2 -0
  58. package/templates/treasury-multisig/script/DeployTreasury.s.sol +128 -0
  59. package/templates/treasury-multisig/src/KozaTreasury.sol +55 -0
  60. package/templates/treasury-multisig/test/DeployTreasury.t.sol +50 -0
  61. package/templates/treasury-multisig/test/Treasury.t.sol +154 -0
@@ -0,0 +1,501 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.34;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {KozaCollection} from "../src/KozaCollection.sol";
6
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7
+ import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol";
8
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
9
+ import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
10
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
11
+
12
+ /**
13
+ * @title ERC721CollectionTest
14
+ * @notice Unit + fuzz tests for KozaCollection (Phase 1, Template 2).
15
+ * @dev Foundry suite covering constructor, allowlist mint, public mint, phase control,
16
+ * per-wallet cap, max supply, royalty, withdraw, ownership.
17
+ * Invariant tests are in ERC721Collection.invariants.t.sol.
18
+ */
19
+ contract ERC721CollectionTest is Test {
20
+ /*//////////////////////////////////////////////////////////////
21
+ STATE
22
+ //////////////////////////////////////////////////////////////*/
23
+
24
+ KozaCollection internal nft;
25
+
26
+ address internal owner;
27
+ address internal alice;
28
+ address internal bob;
29
+ address internal charlie;
30
+ address internal royalty;
31
+ address payable internal treasury;
32
+
33
+ string internal constant NAME = "Koza Genesis";
34
+ string internal constant SYMBOL = "KOZA";
35
+ string internal constant BASE_URI = "ipfs://QmExampleCID/";
36
+ uint256 internal constant MAX_SUPPLY = 100;
37
+ uint256 internal constant MINT_PRICE = 0.1 ether;
38
+ uint96 internal constant ROYALTY_BPS = 500; // %5
39
+
40
+ /*//////////////////////////////////////////////////////////////
41
+ SETUP
42
+ //////////////////////////////////////////////////////////////*/
43
+
44
+ function setUp() public {
45
+ owner = makeAddr("owner");
46
+ alice = makeAddr("alice");
47
+ bob = makeAddr("bob");
48
+ charlie = makeAddr("charlie");
49
+ royalty = makeAddr("royalty");
50
+ treasury = payable(makeAddr("treasury"));
51
+
52
+ nft = new KozaCollection(NAME, SYMBOL, BASE_URI, MAX_SUPPLY, MINT_PRICE, royalty, ROYALTY_BPS, owner);
53
+ }
54
+
55
+ /*//////////////////////////////////////////////////////////////
56
+ CONSTRUCTOR
57
+ //////////////////////////////////////////////////////////////*/
58
+
59
+ function test_Constructor_SetsMetadata() public view {
60
+ assertEq(nft.name(), NAME);
61
+ assertEq(nft.symbol(), SYMBOL);
62
+ assertEq(nft.maxSupply(), MAX_SUPPLY);
63
+ assertEq(nft.mintPrice(), MINT_PRICE);
64
+ assertEq(nft.totalMinted(), 0);
65
+ assertEq(nft.owner(), owner);
66
+ assertEq(nft.pendingOwner(), address(0));
67
+ assertEq(uint8(nft.phase()), uint8(KozaCollection.Phase.Closed));
68
+ assertEq(nft.merkleRoot(), bytes32(0));
69
+
70
+ (address rcv, uint256 amount) = nft.royaltyInfo(1, 10_000);
71
+ assertEq(rcv, royalty);
72
+ assertEq(amount, 500); // %5 of 10000
73
+ }
74
+
75
+ function test_Constructor_SupportsInterfaces() public view {
76
+ assertTrue(nft.supportsInterface(type(IERC721).interfaceId));
77
+ assertTrue(nft.supportsInterface(type(IERC2981).interfaceId));
78
+ assertTrue(nft.supportsInterface(type(IERC165).interfaceId));
79
+ }
80
+
81
+ function test_RevertWhen_ConstructorMaxSupplyZero() public {
82
+ vm.expectRevert(KozaCollection.MaxSupplyZero.selector);
83
+ new KozaCollection(NAME, SYMBOL, BASE_URI, 0, MINT_PRICE, royalty, ROYALTY_BPS, owner);
84
+ }
85
+
86
+ function test_RevertWhen_ConstructorOwnerIsZero() public {
87
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0)));
88
+ new KozaCollection(NAME, SYMBOL, BASE_URI, MAX_SUPPLY, MINT_PRICE, royalty, ROYALTY_BPS, address(0));
89
+ }
90
+
91
+ function test_RevertWhen_ConstructorRoyaltyTooHigh() public {
92
+ // OZ ERC2981: bps > 10000 reverts ERC2981InvalidDefaultRoyalty
93
+ vm.expectRevert(abi.encodeWithSelector(ERC2981.ERC2981InvalidDefaultRoyalty.selector, 10_001, 10_000));
94
+ new KozaCollection(NAME, SYMBOL, BASE_URI, MAX_SUPPLY, MINT_PRICE, royalty, 10_001, owner);
95
+ }
96
+
97
+ function test_RevertWhen_ConstructorRoyaltyReceiverIsZero() public {
98
+ vm.expectRevert(abi.encodeWithSelector(ERC2981.ERC2981InvalidDefaultRoyaltyReceiver.selector, address(0)));
99
+ new KozaCollection(NAME, SYMBOL, BASE_URI, MAX_SUPPLY, MINT_PRICE, address(0), ROYALTY_BPS, owner);
100
+ }
101
+
102
+ /*//////////////////////////////////////////////////////////////
103
+ PUBLIC MINT
104
+ //////////////////////////////////////////////////////////////*/
105
+
106
+ function test_PublicMint_Succeeds() public {
107
+ _setPhase(KozaCollection.Phase.Public);
108
+ vm.deal(alice, 1 ether);
109
+
110
+ vm.prank(alice);
111
+ nft.publicMint{value: MINT_PRICE * 3}(3);
112
+
113
+ assertEq(nft.balanceOf(alice), 3);
114
+ assertEq(nft.ownerOf(1), alice);
115
+ assertEq(nft.ownerOf(2), alice);
116
+ assertEq(nft.ownerOf(3), alice);
117
+ assertEq(nft.totalMinted(), 3);
118
+ assertEq(nft.mintedPerWallet(alice), 3);
119
+ assertEq(address(nft).balance, MINT_PRICE * 3);
120
+ }
121
+
122
+ function test_PublicMint_TokenURI() public {
123
+ _setPhase(KozaCollection.Phase.Public);
124
+ vm.deal(alice, 1 ether);
125
+ vm.prank(alice);
126
+ nft.publicMint{value: MINT_PRICE}(1);
127
+
128
+ assertEq(nft.tokenURI(1), string(abi.encodePacked(BASE_URI, "1")));
129
+ }
130
+
131
+ function test_RevertWhen_PublicMintInClosedPhase() public {
132
+ vm.deal(alice, 1 ether);
133
+ vm.expectRevert(
134
+ abi.encodeWithSelector(
135
+ KozaCollection.WrongPhase.selector, KozaCollection.Phase.Closed, KozaCollection.Phase.Public
136
+ )
137
+ );
138
+ vm.prank(alice);
139
+ nft.publicMint{value: MINT_PRICE}(1);
140
+ }
141
+
142
+ function test_RevertWhen_PublicMintInAllowlistPhase() public {
143
+ _setPhase(KozaCollection.Phase.Allowlist);
144
+ vm.deal(alice, 1 ether);
145
+ vm.expectRevert(
146
+ abi.encodeWithSelector(
147
+ KozaCollection.WrongPhase.selector, KozaCollection.Phase.Allowlist, KozaCollection.Phase.Public
148
+ )
149
+ );
150
+ vm.prank(alice);
151
+ nft.publicMint{value: MINT_PRICE}(1);
152
+ }
153
+
154
+ function test_RevertWhen_PublicMintZeroQuantity() public {
155
+ _setPhase(KozaCollection.Phase.Public);
156
+ vm.expectRevert(KozaCollection.ZeroQuantity.selector);
157
+ vm.prank(alice);
158
+ nft.publicMint{value: 0}(0);
159
+ }
160
+
161
+ function test_RevertWhen_PublicMintIncorrectPayment() public {
162
+ _setPhase(KozaCollection.Phase.Public);
163
+ vm.deal(alice, 1 ether);
164
+ vm.expectRevert(abi.encodeWithSelector(KozaCollection.IncorrectPayment.selector, 0, MINT_PRICE * 2));
165
+ vm.prank(alice);
166
+ nft.publicMint{value: 0}(2);
167
+ }
168
+
169
+ function test_RevertWhen_PublicMintOverpaid() public {
170
+ _setPhase(KozaCollection.Phase.Public);
171
+ vm.deal(alice, 1 ether);
172
+ vm.expectRevert(abi.encodeWithSelector(KozaCollection.IncorrectPayment.selector, MINT_PRICE * 2, MINT_PRICE));
173
+ vm.prank(alice);
174
+ nft.publicMint{value: MINT_PRICE * 2}(1);
175
+ }
176
+
177
+ function test_RevertWhen_PublicMintExceedsPerWalletLimit() public {
178
+ _setPhase(KozaCollection.Phase.Public);
179
+ vm.deal(alice, 100 ether);
180
+
181
+ vm.startPrank(alice);
182
+ nft.publicMint{value: MINT_PRICE * 10}(10); // hits MAX_PER_WALLET
183
+ vm.expectRevert(abi.encodeWithSelector(KozaCollection.ExceedsPerWalletLimit.selector, 10, 1, 10));
184
+ nft.publicMint{value: MINT_PRICE}(1);
185
+ vm.stopPrank();
186
+ }
187
+
188
+ function test_RevertWhen_PublicMintExceedsMaxSupply() public {
189
+ // Deploy a smaller collection so we can exhaust supply within MAX_PER_WALLET
190
+ KozaCollection small = new KozaCollection(NAME, SYMBOL, BASE_URI, 5, MINT_PRICE, royalty, ROYALTY_BPS, owner);
191
+ vm.prank(owner);
192
+ small.setPhase(KozaCollection.Phase.Public);
193
+
194
+ vm.deal(alice, 10 ether);
195
+ vm.deal(bob, 10 ether);
196
+
197
+ vm.prank(alice);
198
+ small.publicMint{value: MINT_PRICE * 5}(5);
199
+
200
+ vm.expectRevert(abi.encodeWithSelector(KozaCollection.MaxSupplyReached.selector, 5, 6));
201
+ vm.prank(bob);
202
+ small.publicMint{value: MINT_PRICE}(1);
203
+ }
204
+
205
+ /*//////////////////////////////////////////////////////////////
206
+ ALLOWLIST MINT
207
+ //////////////////////////////////////////////////////////////*/
208
+
209
+ function test_AllowlistMint_Succeeds() public {
210
+ // 2-yapraklı ağaç: alice + bob
211
+ bytes32 root = _buildTwoLeafRoot(alice, bob);
212
+ vm.startPrank(owner);
213
+ nft.setMerkleRoot(root);
214
+ nft.setPhase(KozaCollection.Phase.Allowlist);
215
+ vm.stopPrank();
216
+
217
+ bytes32[] memory proof = _twoLeafProof(bob);
218
+ vm.deal(alice, 1 ether);
219
+
220
+ vm.prank(alice);
221
+ nft.allowlistMint{value: MINT_PRICE * 2}(2, proof);
222
+
223
+ assertEq(nft.balanceOf(alice), 2);
224
+ assertTrue(nft.allowlistClaimed(alice));
225
+ }
226
+
227
+ function test_RevertWhen_AllowlistMintWrongPhase() public {
228
+ bytes32[] memory proof = new bytes32[](0);
229
+ vm.expectRevert(
230
+ abi.encodeWithSelector(
231
+ KozaCollection.WrongPhase.selector, KozaCollection.Phase.Closed, KozaCollection.Phase.Allowlist
232
+ )
233
+ );
234
+ vm.prank(alice);
235
+ nft.allowlistMint{value: 0}(1, proof);
236
+ }
237
+
238
+ function test_RevertWhen_AllowlistMintInvalidProof() public {
239
+ bytes32 root = _buildTwoLeafRoot(alice, bob);
240
+ vm.startPrank(owner);
241
+ nft.setMerkleRoot(root);
242
+ nft.setPhase(KozaCollection.Phase.Allowlist);
243
+ vm.stopPrank();
244
+
245
+ // charlie listede değil
246
+ bytes32[] memory proof = _twoLeafProof(bob);
247
+ vm.deal(charlie, 1 ether);
248
+ vm.expectRevert(KozaCollection.InvalidProof.selector);
249
+ vm.prank(charlie);
250
+ nft.allowlistMint{value: MINT_PRICE}(1, proof);
251
+ }
252
+
253
+ function test_RevertWhen_AllowlistAlreadyClaimed() public {
254
+ bytes32 root = _buildTwoLeafRoot(alice, bob);
255
+ vm.startPrank(owner);
256
+ nft.setMerkleRoot(root);
257
+ nft.setPhase(KozaCollection.Phase.Allowlist);
258
+ vm.stopPrank();
259
+
260
+ bytes32[] memory proof = _twoLeafProof(bob);
261
+ vm.deal(alice, 1 ether);
262
+
263
+ vm.startPrank(alice);
264
+ nft.allowlistMint{value: MINT_PRICE}(1, proof);
265
+ vm.expectRevert(abi.encodeWithSelector(KozaCollection.AlreadyClaimed.selector, alice));
266
+ nft.allowlistMint{value: MINT_PRICE}(1, proof);
267
+ vm.stopPrank();
268
+ }
269
+
270
+ function test_AllowlistMintRespectsMaxPerWallet() public {
271
+ bytes32 root = _buildTwoLeafRoot(alice, bob);
272
+ vm.startPrank(owner);
273
+ nft.setMerkleRoot(root);
274
+ nft.setPhase(KozaCollection.Phase.Allowlist);
275
+ vm.stopPrank();
276
+
277
+ bytes32[] memory proof = _twoLeafProof(bob);
278
+ vm.deal(alice, 10 ether);
279
+
280
+ vm.prank(alice);
281
+ vm.expectRevert(abi.encodeWithSelector(KozaCollection.ExceedsPerWalletLimit.selector, 0, 11, 10));
282
+ nft.allowlistMint{value: MINT_PRICE * 11}(11, proof);
283
+ }
284
+
285
+ /*//////////////////////////////////////////////////////////////
286
+ OWNER FUNCTIONS
287
+ //////////////////////////////////////////////////////////////*/
288
+
289
+ function test_SetMerkleRoot_OnlyOwner() public {
290
+ bytes32 root = bytes32(uint256(0xCAFEBABE));
291
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
292
+ vm.prank(alice);
293
+ nft.setMerkleRoot(root);
294
+
295
+ vm.expectEmit(true, false, false, false);
296
+ emit KozaCollection.MerkleRootSet(root);
297
+ vm.prank(owner);
298
+ nft.setMerkleRoot(root);
299
+ assertEq(nft.merkleRoot(), root);
300
+ }
301
+
302
+ function test_SetPhase_OnlyOwner() public {
303
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
304
+ vm.prank(alice);
305
+ nft.setPhase(KozaCollection.Phase.Public);
306
+
307
+ vm.expectEmit(true, false, false, false);
308
+ emit KozaCollection.PhaseChanged(KozaCollection.Phase.Public);
309
+ vm.prank(owner);
310
+ nft.setPhase(KozaCollection.Phase.Public);
311
+ assertEq(uint8(nft.phase()), uint8(KozaCollection.Phase.Public));
312
+ }
313
+
314
+ function test_SetBaseURI_OnlyOwner() public {
315
+ string memory newURI = "https://meta.koza.dev/";
316
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
317
+ vm.prank(alice);
318
+ nft.setBaseURI(newURI);
319
+
320
+ vm.prank(owner);
321
+ nft.setBaseURI(newURI);
322
+
323
+ // Mint and verify URI uses new prefix
324
+ vm.prank(owner);
325
+ nft.setPhase(KozaCollection.Phase.Public);
326
+ vm.deal(alice, 1 ether);
327
+ vm.prank(alice);
328
+ nft.publicMint{value: MINT_PRICE}(1);
329
+ assertEq(nft.tokenURI(1), string(abi.encodePacked(newURI, "1")));
330
+ }
331
+
332
+ function test_SetMintPrice_OnlyOwner() public {
333
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
334
+ vm.prank(alice);
335
+ nft.setMintPrice(0.5 ether);
336
+
337
+ vm.prank(owner);
338
+ nft.setMintPrice(0.5 ether);
339
+ assertEq(nft.mintPrice(), 0.5 ether);
340
+ }
341
+
342
+ function test_SetDefaultRoyalty_OnlyOwner() public {
343
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
344
+ vm.prank(alice);
345
+ nft.setDefaultRoyalty(charlie, 1000);
346
+
347
+ vm.prank(owner);
348
+ nft.setDefaultRoyalty(charlie, 1000); // %10
349
+ (address rcv, uint256 amount) = nft.royaltyInfo(1, 10_000);
350
+ assertEq(rcv, charlie);
351
+ assertEq(amount, 1000);
352
+ }
353
+
354
+ /*//////////////////////////////////////////////////////////////
355
+ WITHDRAW
356
+ //////////////////////////////////////////////////////////////*/
357
+
358
+ function test_Withdraw_Succeeds() public {
359
+ _setPhase(KozaCollection.Phase.Public);
360
+ vm.deal(alice, 1 ether);
361
+ vm.prank(alice);
362
+ nft.publicMint{value: MINT_PRICE * 5}(5);
363
+
364
+ uint256 balanceBefore = treasury.balance;
365
+ vm.expectEmit(true, false, false, true);
366
+ emit KozaCollection.Withdrawn(treasury, MINT_PRICE * 5);
367
+ vm.prank(owner);
368
+ nft.withdraw(treasury);
369
+
370
+ assertEq(treasury.balance, balanceBefore + MINT_PRICE * 5);
371
+ assertEq(address(nft).balance, 0);
372
+ }
373
+
374
+ function test_Withdraw_OnlyOwner() public {
375
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
376
+ vm.prank(alice);
377
+ nft.withdraw(treasury);
378
+ }
379
+
380
+ function test_RevertWhen_WithdrawToRejectingContract() public {
381
+ _setPhase(KozaCollection.Phase.Public);
382
+ vm.deal(alice, 1 ether);
383
+ vm.prank(alice);
384
+ nft.publicMint{value: MINT_PRICE}(1);
385
+
386
+ RejectingReceiver rejecter = new RejectingReceiver();
387
+ vm.expectRevert(KozaCollection.WithdrawFailed.selector);
388
+ vm.prank(owner);
389
+ nft.withdraw(payable(address(rejecter)));
390
+ }
391
+
392
+ /*//////////////////////////////////////////////////////////////
393
+ OWNERSHIP TRANSFER
394
+ //////////////////////////////////////////////////////////////*/
395
+
396
+ function test_OwnershipTransfer_TwoStep() public {
397
+ vm.prank(owner);
398
+ nft.transferOwnership(alice);
399
+ assertEq(nft.owner(), owner);
400
+ assertEq(nft.pendingOwner(), alice);
401
+
402
+ vm.prank(alice);
403
+ nft.acceptOwnership();
404
+ assertEq(nft.owner(), alice);
405
+ assertEq(nft.pendingOwner(), address(0));
406
+ }
407
+
408
+ function test_OwnershipTransfer_CancelByNewTransfer() public {
409
+ vm.prank(owner);
410
+ nft.transferOwnership(alice);
411
+
412
+ // Owner overrides pendingOwner with another address
413
+ vm.prank(owner);
414
+ nft.transferOwnership(bob);
415
+ assertEq(nft.pendingOwner(), bob);
416
+
417
+ // Alice can no longer accept
418
+ vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
419
+ vm.prank(alice);
420
+ nft.acceptOwnership();
421
+ }
422
+
423
+ /*//////////////////////////////////////////////////////////////
424
+ FUZZ
425
+ //////////////////////////////////////////////////////////////*/
426
+
427
+ function testFuzz_PublicMint_QuantityWithinLimits(uint256 qty) public {
428
+ qty = bound(qty, 1, 10);
429
+ _setPhase(KozaCollection.Phase.Public);
430
+ uint256 cost = MINT_PRICE * qty;
431
+ vm.deal(alice, cost);
432
+
433
+ vm.prank(alice);
434
+ nft.publicMint{value: cost}(qty);
435
+ assertEq(nft.balanceOf(alice), qty);
436
+ assertEq(nft.totalMinted(), qty);
437
+ }
438
+
439
+ function testFuzz_AllowlistMint_DoesNotAllowDoubleClaim(uint256 qty) public {
440
+ qty = bound(qty, 1, 5);
441
+ bytes32 root = _buildTwoLeafRoot(alice, bob);
442
+ vm.startPrank(owner);
443
+ nft.setMerkleRoot(root);
444
+ nft.setPhase(KozaCollection.Phase.Allowlist);
445
+ vm.stopPrank();
446
+
447
+ bytes32[] memory proof = _twoLeafProof(bob);
448
+ vm.deal(alice, 10 ether);
449
+
450
+ vm.startPrank(alice);
451
+ nft.allowlistMint{value: MINT_PRICE * qty}(qty, proof);
452
+ vm.expectRevert(abi.encodeWithSelector(KozaCollection.AlreadyClaimed.selector, alice));
453
+ nft.allowlistMint{value: MINT_PRICE}(1, proof);
454
+ vm.stopPrank();
455
+ }
456
+
457
+ function testFuzz_RoyaltyCalculation(uint96 bps, uint256 salePrice) public {
458
+ bps = uint96(bound(bps, 0, 10_000));
459
+ salePrice = bound(salePrice, 0, type(uint128).max);
460
+
461
+ vm.prank(owner);
462
+ nft.setDefaultRoyalty(charlie, bps);
463
+
464
+ (address rcv, uint256 amount) = nft.royaltyInfo(1, salePrice);
465
+ assertEq(rcv, charlie);
466
+ assertEq(amount, salePrice * bps / 10_000);
467
+ }
468
+
469
+ /*//////////////////////////////////////////////////////////////
470
+ HELPERS
471
+ //////////////////////////////////////////////////////////////*/
472
+
473
+ function _setPhase(KozaCollection.Phase p) internal {
474
+ vm.prank(owner);
475
+ nft.setPhase(p);
476
+ }
477
+
478
+ /// @dev OZ MerkleProof commutative hash kullanır (sıralanmış pair). 2 yaprak için
479
+ /// kök = keccak256(sorted(leaf1, leaf2)).
480
+ function _buildTwoLeafRoot(address a, address b) internal pure returns (bytes32) {
481
+ bytes32 leafA = keccak256(abi.encodePacked(a));
482
+ bytes32 leafB = keccak256(abi.encodePacked(b));
483
+ return _commutativeHash(leafA, leafB);
484
+ }
485
+
486
+ /// @dev `addr` için kanıt = diğer yaprağın hash'i (2 yapraklı ağaçta).
487
+ function _twoLeafProof(address other) internal pure returns (bytes32[] memory) {
488
+ bytes32[] memory proof = new bytes32[](1);
489
+ proof[0] = keccak256(abi.encodePacked(other));
490
+ return proof;
491
+ }
492
+
493
+ function _commutativeHash(bytes32 a, bytes32 b) internal pure returns (bytes32) {
494
+ return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));
495
+ }
496
+ }
497
+
498
+ /// @dev `receive`/`fallback` olmayan kontrat — withdraw target reddi senaryosu için.
499
+ contract RejectingReceiver {
500
+ // no receive/fallback → ETH transfer fails
501
+ }
@@ -0,0 +1,19 @@
1
+ # ============================================
2
+ # Bu dosyayı kopyalayıp `.env` olarak adlandırın. `.env` ASLA commit edilmez.
3
+ # ============================================
4
+
5
+ # ---- Deploy Wallet (Fuji testnet) ----
6
+ # UYARI: Mainnet private key'i ASLA buraya koyma. Sadece testnet için.
7
+ # Production'da Foundry Cast Wallet (encrypted keystore) veya hardware wallet kullan.
8
+ PRIVATE_KEY=
9
+
10
+ # Deployer adres (sanity check, opsiyonel)
11
+ DEPLOYER_ADDRESS=
12
+
13
+ # ---- Block Explorer (Routescan) ----
14
+ # routescan.io'dan ücretsiz `rs_` prefix'li API key al. `--verify` için gerekir.
15
+ SNOWTRACE_API_KEY=
16
+
17
+ # ---- ICM / Teleporter ----
18
+ # Avalanche Teleporter messenger — tüm L1'lerde deterministic adres.
19
+ TELEPORTER_MESSENGER_ADDRESS=0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf
@@ -0,0 +1,9 @@
1
+ [submodule "lib/forge-std"]
2
+ path = lib/forge-std
3
+ url = https://github.com/foundry-rs/forge-std
4
+ [submodule "lib/openzeppelin-contracts"]
5
+ path = lib/openzeppelin-contracts
6
+ url = https://github.com/OpenZeppelin/openzeppelin-contracts
7
+ [submodule "lib/icm-contracts"]
8
+ path = lib/icm-contracts
9
+ url = https://github.com/ava-labs/icm-contracts
@@ -0,0 +1,49 @@
1
+ # ICTT Bridge (Home + Remote)
2
+
3
+ Avalanche Interchain Token Transfer ile C-Chain ↔ L1 token köprüsü (KozaTokenHome + KozaTokenRemote).
4
+
5
+ Bu, kozalak-l1 deposundan üretilmiş **standalone** bir Foundry projesidir;
6
+ kendi başına derlenir ve test edilir.
7
+
8
+ ## Kurulum
9
+
10
+ > **Windows notu:** icm-contracts'ın iç içe submodule yolları uzundur;
11
+ > önce uzun-yol desteğini aç: `git config --system core.longpaths true`
12
+
13
+ ```bash
14
+ forge install foundry-rs/forge-std@8987040ede9553cea20c95ad40d0455930f9c8e0 OpenZeppelin/openzeppelin-contracts@e4f70216d759d8e6a64144a9e1f7bbeed78e7079 ava-labs/icm-contracts@dac65983fb956586aebeadab1c4290d2f87927b4
15
+ # icm-contracts'ın iç bağımlılıkları (oz-upgradeable, subnet-evm) için:
16
+ git -C lib/icm-contracts submodule update --init --recursive
17
+ forge build
18
+ forge test
19
+ ```
20
+
21
+ > `forge install ...@<commit>` bağımlılıkları repo ile birebir aynı commit'lere
22
+ > pinler (aşağıdaki tabloya bakın). `.gitmodules` ve `remappings.txt`
23
+ > bu pinlerle uyumludur.
24
+
25
+ ## Deploy
26
+
27
+ Bu şablon (ICTT köprüsü) çok-adımlı, iki-zincirli bir kurulum gerektirir
28
+ (Home + Remote + Teleporter registry). Otomatik tek-komut deploy YOKTUR.
29
+
30
+ Adım adım rehber: kozalak-l1 deposundaki
31
+ `docs/tr/03-templateler/ictt-bridge.md` dosyasını izleyin.
32
+
33
+ ## Yeniden adlandırma
34
+
35
+ Contract'ı kendi adınla yeniden adlandırmak istersen `src/`, `test/` ve
36
+ `script/` altındaki dosyalarda contract/dosya adını birlikte güncelle
37
+ (import'lar tek-seviye relative olduğu için tutarlı kalmalı).
38
+
39
+ ## Bağımlılık pinleri
40
+
41
+ | Bağımlılık | Tag | Commit (pin) |
42
+ | --- | --- | --- |
43
+ | `forge-std` | v1.16.0 | `8987040ede9553cea20c95ad40d0455930f9c8e0` |
44
+ | `openzeppelin-contracts` | v5.3.0 | `e4f70216d759d8e6a64144a9e1f7bbeed78e7079` |
45
+ | `icm-contracts` | v1.0.9 | `dac65983fb956586aebeadab1c4290d2f87927b4` |
46
+
47
+ ---
48
+
49
+ _Bu proje `create-kozalak-l1` tarafından üretildi. Kaynak: kozalak-l1 mono-repo._
@@ -0,0 +1,41 @@
1
+ [profile.default]
2
+ src = "src"
3
+ out = "out"
4
+ libs = ["lib"]
5
+ test = "test"
6
+ script = "script"
7
+ # Bu şablon solc 0.8.25 pragma'sı kullanır. auto_detect_solc dosya
8
+ # pragma'sına göre doğru derleyiciyi otomatik indirir/seçer.
9
+ auto_detect_solc = true
10
+ optimizer = true
11
+ optimizer_runs = 200
12
+ via_ir = true
13
+ bytecode_hash = "none"
14
+ cbor_metadata = false
15
+ remappings = [
16
+ "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
17
+ "forge-std/=lib/forge-std/src/",
18
+ "icm-contracts/=lib/icm-contracts/",
19
+ "@subnet-evm/=lib/icm-contracts/contracts/subnet-evm/",
20
+ "@teleporter/=lib/icm-contracts/contracts/teleporter/",
21
+ "@utilities/=lib/icm-contracts/contracts/utilities/",
22
+ "@mocks/=lib/icm-contracts/contracts/mocks/",
23
+ "@ictt/=lib/icm-contracts/contracts/ictt/"
24
+ ]
25
+
26
+ [fmt]
27
+ line_length = 120
28
+ tab_width = 4
29
+ bracket_spacing = false
30
+ int_types = "long"
31
+ multiline_func_header = "all"
32
+ quote_style = "double"
33
+ number_underscore = "thousands"
34
+
35
+ [rpc_endpoints]
36
+ fuji = "https://api.avax-test.network/ext/bc/C/rpc"
37
+ avalanche = "https://api.avax.network/ext/bc/C/rpc"
38
+
39
+ [etherscan]
40
+ fuji = { key = "${SNOWTRACE_API_KEY}", url = "https://api.routescan.io/v2/network/testnet/evm/43113/etherscan", chain = 43113 }
41
+ avalanche = { key = "${SNOWTRACE_API_KEY}", url = "https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan", chain = 43114 }
@@ -0,0 +1,5 @@
1
+ .env
2
+ out/
3
+ cache/
4
+ broadcast/
5
+ lib/
@@ -0,0 +1,8 @@
1
+ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
2
+ forge-std/=lib/forge-std/src/
3
+ icm-contracts/=lib/icm-contracts/
4
+ @subnet-evm/=lib/icm-contracts/contracts/subnet-evm/
5
+ @teleporter/=lib/icm-contracts/contracts/teleporter/
6
+ @utilities/=lib/icm-contracts/contracts/utilities/
7
+ @mocks/=lib/icm-contracts/contracts/mocks/
8
+ @ictt/=lib/icm-contracts/contracts/ictt/