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.
- package/README.md +87 -0
- package/dist/deploy.js +63 -0
- package/dist/forge.js +27 -0
- package/dist/index.js +176 -0
- package/dist/prompts.js +23 -0
- package/dist/scaffold.js +33 -0
- package/dist/templates.js +108 -0
- package/package.json +29 -0
- package/templates/erc20-gas/.env.example +19 -0
- package/templates/erc20-gas/.gitmodules +6 -0
- package/templates/erc20-gas/README.md +48 -0
- package/templates/erc20-gas/foundry.toml +35 -0
- package/templates/erc20-gas/gitignore +5 -0
- package/templates/erc20-gas/remappings.txt +2 -0
- package/templates/erc20-gas/script/DeployERC20Gas.s.sol +131 -0
- package/templates/erc20-gas/src/KozaGasToken.sol +105 -0
- package/templates/erc20-gas/test/DeployERC20Gas.t.sol +60 -0
- package/templates/erc20-gas/test/ERC20Gas.invariants.t.sol +144 -0
- package/templates/erc20-gas/test/ERC20Gas.t.sol +354 -0
- package/templates/erc721-collection/.env.example +37 -0
- package/templates/erc721-collection/.gitmodules +6 -0
- package/templates/erc721-collection/README.md +48 -0
- package/templates/erc721-collection/foundry.toml +35 -0
- package/templates/erc721-collection/gitignore +5 -0
- package/templates/erc721-collection/remappings.txt +2 -0
- package/templates/erc721-collection/script/DeployERC721Collection.s.sol +151 -0
- package/templates/erc721-collection/src/KozaCollection.sol +281 -0
- package/templates/erc721-collection/test/DeployERC721Collection.t.sol +76 -0
- package/templates/erc721-collection/test/ERC721Collection.invariants.t.sol +175 -0
- package/templates/erc721-collection/test/ERC721Collection.t.sol +501 -0
- package/templates/ictt-bridge/.env.example +19 -0
- package/templates/ictt-bridge/.gitmodules +9 -0
- package/templates/ictt-bridge/README.md +49 -0
- package/templates/ictt-bridge/foundry.toml +41 -0
- package/templates/ictt-bridge/gitignore +5 -0
- package/templates/ictt-bridge/remappings.txt +8 -0
- package/templates/ictt-bridge/script/DeployTokenHome.s.sol +139 -0
- package/templates/ictt-bridge/src/KozaTokenHome.sol +57 -0
- package/templates/ictt-bridge/src/KozaTokenRemote.sol +65 -0
- package/templates/ictt-bridge/test/ICTTBridge.t.sol +157 -0
- package/templates/soulbound-credential/.env.example +19 -0
- package/templates/soulbound-credential/.gitmodules +6 -0
- package/templates/soulbound-credential/README.md +48 -0
- package/templates/soulbound-credential/foundry.toml +35 -0
- package/templates/soulbound-credential/gitignore +5 -0
- package/templates/soulbound-credential/remappings.txt +2 -0
- package/templates/soulbound-credential/script/DeployCredential.s.sol +126 -0
- package/templates/soulbound-credential/src/KozaCredential.sol +201 -0
- package/templates/soulbound-credential/test/DeployCredential.t.sol +46 -0
- package/templates/soulbound-credential/test/Soulbound.invariants.t.sol +133 -0
- package/templates/soulbound-credential/test/Soulbound.t.sol +319 -0
- package/templates/treasury-multisig/.env.example +19 -0
- package/templates/treasury-multisig/.gitmodules +6 -0
- package/templates/treasury-multisig/README.md +48 -0
- package/templates/treasury-multisig/foundry.toml +35 -0
- package/templates/treasury-multisig/gitignore +5 -0
- package/templates/treasury-multisig/remappings.txt +2 -0
- package/templates/treasury-multisig/script/DeployTreasury.s.sol +128 -0
- package/templates/treasury-multisig/src/KozaTreasury.sol +55 -0
- package/templates/treasury-multisig/test/DeployTreasury.t.sol +50 -0
- 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,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/
|