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,354 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.34;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {KozaGasToken} from "../src/KozaGasToken.sol";
|
|
6
|
+
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
|
|
7
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
8
|
+
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @title ERC20GasTest
|
|
12
|
+
* @notice Unit + fuzz tests for KozaGasToken (Phase 1, Template 1).
|
|
13
|
+
* @dev Foundry test suite covering constructor, mint, burn, ERC-20 transfer, permit, ownership.
|
|
14
|
+
* Invariant tests are in ERC20Gas.invariants.t.sol.
|
|
15
|
+
*/
|
|
16
|
+
contract ERC20GasTest is Test {
|
|
17
|
+
/*//////////////////////////////////////////////////////////////
|
|
18
|
+
STATE
|
|
19
|
+
//////////////////////////////////////////////////////////////*/
|
|
20
|
+
|
|
21
|
+
KozaGasToken internal token;
|
|
22
|
+
|
|
23
|
+
address internal owner;
|
|
24
|
+
uint256 internal ownerKey;
|
|
25
|
+
address internal alice;
|
|
26
|
+
uint256 internal aliceKey;
|
|
27
|
+
address internal bob;
|
|
28
|
+
address internal charlie;
|
|
29
|
+
|
|
30
|
+
string internal constant NAME = "Koza Gas Token";
|
|
31
|
+
string internal constant SYMBOL = "KGAS";
|
|
32
|
+
uint256 internal constant CAP = 1_000_000 ether;
|
|
33
|
+
uint256 internal constant INITIAL_MINT = 100_000 ether;
|
|
34
|
+
|
|
35
|
+
/*//////////////////////////////////////////////////////////////
|
|
36
|
+
SETUP
|
|
37
|
+
//////////////////////////////////////////////////////////////*/
|
|
38
|
+
|
|
39
|
+
function setUp() public {
|
|
40
|
+
(owner, ownerKey) = makeAddrAndKey("owner");
|
|
41
|
+
(alice, aliceKey) = makeAddrAndKey("alice");
|
|
42
|
+
bob = makeAddr("bob");
|
|
43
|
+
charlie = makeAddr("charlie");
|
|
44
|
+
|
|
45
|
+
token = new KozaGasToken(NAME, SYMBOL, CAP, INITIAL_MINT, owner);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*//////////////////////////////////////////////////////////////
|
|
49
|
+
CONSTRUCTOR
|
|
50
|
+
//////////////////////////////////////////////////////////////*/
|
|
51
|
+
|
|
52
|
+
function test_Constructor_SetsMetadata() public view {
|
|
53
|
+
assertEq(token.name(), NAME);
|
|
54
|
+
assertEq(token.symbol(), SYMBOL);
|
|
55
|
+
assertEq(token.decimals(), 18);
|
|
56
|
+
assertEq(token.cap(), CAP);
|
|
57
|
+
assertEq(token.totalSupply(), INITIAL_MINT);
|
|
58
|
+
assertEq(token.balanceOf(owner), INITIAL_MINT);
|
|
59
|
+
assertEq(token.owner(), owner);
|
|
60
|
+
assertEq(token.pendingOwner(), address(0));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function test_Constructor_DeploysWithoutInitialMint() public {
|
|
64
|
+
KozaGasToken t = new KozaGasToken(NAME, SYMBOL, CAP, 0, owner);
|
|
65
|
+
assertEq(t.totalSupply(), 0);
|
|
66
|
+
assertEq(t.balanceOf(owner), 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function test_RevertWhen_ConstructorOwnerIsZero() public {
|
|
70
|
+
// Ownable parent rejects zero address before our body executes
|
|
71
|
+
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0)));
|
|
72
|
+
new KozaGasToken(NAME, SYMBOL, CAP, INITIAL_MINT, address(0));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function test_RevertWhen_ConstructorCapIsZero() public {
|
|
76
|
+
// ERC20Capped parent rejects zero cap before our body executes
|
|
77
|
+
vm.expectRevert(abi.encodeWithSelector(ERC20Capped.ERC20InvalidCap.selector, 0));
|
|
78
|
+
new KozaGasToken(NAME, SYMBOL, 0, 0, owner);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function test_RevertWhen_InitialMintExceedsCap() public {
|
|
82
|
+
uint256 tooMuch = CAP + 1;
|
|
83
|
+
vm.expectRevert(abi.encodeWithSelector(KozaGasToken.InitialMintExceedsCap.selector, CAP, tooMuch));
|
|
84
|
+
new KozaGasToken(NAME, SYMBOL, CAP, tooMuch, owner);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/*//////////////////////////////////////////////////////////////
|
|
88
|
+
MINT
|
|
89
|
+
//////////////////////////////////////////////////////////////*/
|
|
90
|
+
|
|
91
|
+
function test_Mint_OwnerCanMint() public {
|
|
92
|
+
uint256 amount = 50_000 ether;
|
|
93
|
+
vm.prank(owner);
|
|
94
|
+
token.mint(alice, amount);
|
|
95
|
+
|
|
96
|
+
assertEq(token.balanceOf(alice), amount);
|
|
97
|
+
assertEq(token.totalSupply(), INITIAL_MINT + amount);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function test_RevertWhen_NonOwnerMints() public {
|
|
101
|
+
vm.prank(alice);
|
|
102
|
+
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
|
|
103
|
+
token.mint(alice, 1 ether);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function test_RevertWhen_MintToZeroAddress() public {
|
|
107
|
+
// ERC20 _mint rejects zero address with ERC20InvalidReceiver
|
|
108
|
+
vm.prank(owner);
|
|
109
|
+
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidReceiver.selector, address(0)));
|
|
110
|
+
token.mint(address(0), 1 ether);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function test_RevertWhen_MintZeroAmount() public {
|
|
114
|
+
vm.prank(owner);
|
|
115
|
+
vm.expectRevert(KozaGasToken.ZeroAmount.selector);
|
|
116
|
+
token.mint(alice, 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function test_RevertWhen_MintExceedsCap() public {
|
|
120
|
+
uint256 remaining = CAP - INITIAL_MINT;
|
|
121
|
+
uint256 tooMuch = remaining + 1;
|
|
122
|
+
|
|
123
|
+
vm.prank(owner);
|
|
124
|
+
vm.expectRevert(abi.encodeWithSelector(ERC20Capped.ERC20ExceededCap.selector, INITIAL_MINT + tooMuch, CAP));
|
|
125
|
+
token.mint(alice, tooMuch);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function test_Mint_AtExactCapBoundary() public {
|
|
129
|
+
uint256 remaining = CAP - INITIAL_MINT;
|
|
130
|
+
vm.prank(owner);
|
|
131
|
+
token.mint(alice, remaining);
|
|
132
|
+
|
|
133
|
+
assertEq(token.totalSupply(), CAP);
|
|
134
|
+
assertEq(token.balanceOf(alice), remaining);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/*//////////////////////////////////////////////////////////////
|
|
138
|
+
BURN
|
|
139
|
+
//////////////////////////////////////////////////////////////*/
|
|
140
|
+
|
|
141
|
+
function test_Burn_HolderCanBurnOwnTokens() public {
|
|
142
|
+
uint256 burnAmount = 10_000 ether;
|
|
143
|
+
|
|
144
|
+
vm.prank(owner);
|
|
145
|
+
token.burn(burnAmount);
|
|
146
|
+
|
|
147
|
+
assertEq(token.balanceOf(owner), INITIAL_MINT - burnAmount);
|
|
148
|
+
assertEq(token.totalSupply(), INITIAL_MINT - burnAmount);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function test_RevertWhen_BurnZeroAmount() public {
|
|
152
|
+
vm.prank(owner);
|
|
153
|
+
vm.expectRevert(KozaGasToken.ZeroAmount.selector);
|
|
154
|
+
token.burn(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function test_RevertWhen_BurnExceedsBalance() public {
|
|
158
|
+
vm.prank(alice);
|
|
159
|
+
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 0, 1 ether));
|
|
160
|
+
token.burn(1 ether);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function test_Burn_FreesCapHeadroom() public {
|
|
164
|
+
// owner burns half, then mints back same amount → still under cap
|
|
165
|
+
uint256 half = INITIAL_MINT / 2;
|
|
166
|
+
|
|
167
|
+
vm.prank(owner);
|
|
168
|
+
token.burn(half);
|
|
169
|
+
|
|
170
|
+
vm.prank(owner);
|
|
171
|
+
token.mint(alice, half);
|
|
172
|
+
|
|
173
|
+
assertEq(token.totalSupply(), INITIAL_MINT);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/*//////////////////////////////////////////////////////////////
|
|
177
|
+
ERC-20 STANDARD
|
|
178
|
+
//////////////////////////////////////////////////////////////*/
|
|
179
|
+
|
|
180
|
+
function test_Transfer_Standard() public {
|
|
181
|
+
vm.prank(owner);
|
|
182
|
+
bool ok = token.transfer(alice, 1000 ether);
|
|
183
|
+
|
|
184
|
+
assertTrue(ok);
|
|
185
|
+
assertEq(token.balanceOf(alice), 1000 ether);
|
|
186
|
+
assertEq(token.balanceOf(owner), INITIAL_MINT - 1000 ether);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function test_Approve_AndTransferFrom() public {
|
|
190
|
+
vm.prank(owner);
|
|
191
|
+
token.approve(alice, 500 ether);
|
|
192
|
+
|
|
193
|
+
assertEq(token.allowance(owner, alice), 500 ether);
|
|
194
|
+
|
|
195
|
+
vm.prank(alice);
|
|
196
|
+
bool ok = token.transferFrom(owner, bob, 500 ether);
|
|
197
|
+
|
|
198
|
+
assertTrue(ok);
|
|
199
|
+
assertEq(token.balanceOf(bob), 500 ether);
|
|
200
|
+
assertEq(token.allowance(owner, alice), 0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/*//////////////////////////////////////////////////////////////
|
|
204
|
+
ERC-2612 PERMIT
|
|
205
|
+
//////////////////////////////////////////////////////////////*/
|
|
206
|
+
|
|
207
|
+
function test_Permit_ValidSignature() public {
|
|
208
|
+
uint256 value = 100 ether;
|
|
209
|
+
uint256 deadline = block.timestamp + 1 hours;
|
|
210
|
+
|
|
211
|
+
// Fund alice first
|
|
212
|
+
vm.prank(owner);
|
|
213
|
+
token.transfer(alice, 100 ether);
|
|
214
|
+
|
|
215
|
+
bytes32 digest = _permitDigest(alice, bob, value, token.nonces(alice), deadline);
|
|
216
|
+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(aliceKey, digest);
|
|
217
|
+
|
|
218
|
+
token.permit(alice, bob, value, deadline, v, r, s);
|
|
219
|
+
|
|
220
|
+
assertEq(token.allowance(alice, bob), value);
|
|
221
|
+
assertEq(token.nonces(alice), 1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function test_RevertWhen_PermitExpired() public {
|
|
225
|
+
// Move forward so deadline=1 (a stale timestamp) is in the past relative to block.timestamp
|
|
226
|
+
vm.warp(2 days);
|
|
227
|
+
uint256 deadline = 1;
|
|
228
|
+
|
|
229
|
+
bytes32 digest = _permitDigest(alice, bob, 100 ether, 0, deadline);
|
|
230
|
+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(aliceKey, digest);
|
|
231
|
+
|
|
232
|
+
vm.expectRevert();
|
|
233
|
+
token.permit(alice, bob, 100 ether, deadline, v, r, s);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function test_RevertWhen_PermitReplayed() public {
|
|
237
|
+
uint256 value = 100 ether;
|
|
238
|
+
uint256 deadline = block.timestamp + 1 hours;
|
|
239
|
+
|
|
240
|
+
bytes32 digest = _permitDigest(alice, bob, value, token.nonces(alice), deadline);
|
|
241
|
+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(aliceKey, digest);
|
|
242
|
+
|
|
243
|
+
token.permit(alice, bob, value, deadline, v, r, s);
|
|
244
|
+
|
|
245
|
+
// Replay should fail (nonce was consumed)
|
|
246
|
+
vm.expectRevert();
|
|
247
|
+
token.permit(alice, bob, value, deadline, v, r, s);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/*//////////////////////////////////////////////////////////////
|
|
251
|
+
OWNABLE2STEP
|
|
252
|
+
//////////////////////////////////////////////////////////////*/
|
|
253
|
+
|
|
254
|
+
function test_TransferOwnership_TwoStep() public {
|
|
255
|
+
// Step 1: owner initiates transfer
|
|
256
|
+
vm.prank(owner);
|
|
257
|
+
token.transferOwnership(alice);
|
|
258
|
+
|
|
259
|
+
assertEq(token.owner(), owner, "owner unchanged before accept");
|
|
260
|
+
assertEq(token.pendingOwner(), alice, "alice is pending");
|
|
261
|
+
|
|
262
|
+
// Step 2: alice accepts
|
|
263
|
+
vm.prank(alice);
|
|
264
|
+
token.acceptOwnership();
|
|
265
|
+
|
|
266
|
+
assertEq(token.owner(), alice, "alice is now owner");
|
|
267
|
+
assertEq(token.pendingOwner(), address(0), "pending cleared");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function test_RevertWhen_NonPendingAcceptsOwnership() public {
|
|
271
|
+
vm.prank(owner);
|
|
272
|
+
token.transferOwnership(alice);
|
|
273
|
+
|
|
274
|
+
vm.prank(bob);
|
|
275
|
+
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, bob));
|
|
276
|
+
token.acceptOwnership();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function test_TransferOwnership_OldOwnerCanCancel() public {
|
|
280
|
+
vm.prank(owner);
|
|
281
|
+
token.transferOwnership(alice);
|
|
282
|
+
|
|
283
|
+
// owner overrides with another transfer (pending update)
|
|
284
|
+
vm.prank(owner);
|
|
285
|
+
token.transferOwnership(charlie);
|
|
286
|
+
|
|
287
|
+
assertEq(token.pendingOwner(), charlie);
|
|
288
|
+
|
|
289
|
+
// alice can no longer accept
|
|
290
|
+
vm.prank(alice);
|
|
291
|
+
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
|
|
292
|
+
token.acceptOwnership();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/*//////////////////////////////////////////////////////////////
|
|
296
|
+
FUZZ
|
|
297
|
+
//////////////////////////////////////////////////////////////*/
|
|
298
|
+
|
|
299
|
+
function testFuzz_Transfer(uint256 amount) public {
|
|
300
|
+
amount = bound(amount, 1, INITIAL_MINT);
|
|
301
|
+
|
|
302
|
+
vm.prank(owner);
|
|
303
|
+
bool ok = token.transfer(alice, amount);
|
|
304
|
+
assertTrue(ok);
|
|
305
|
+
|
|
306
|
+
assertEq(token.balanceOf(alice), amount);
|
|
307
|
+
assertEq(token.balanceOf(owner), INITIAL_MINT - amount);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function testFuzz_MintRespectsCap(uint256 amount) public {
|
|
311
|
+
// Bound to avoid uint256 overflow on INITIAL_MINT + amount
|
|
312
|
+
amount = bound(amount, 1, type(uint128).max);
|
|
313
|
+
|
|
314
|
+
uint256 remaining = CAP - INITIAL_MINT;
|
|
315
|
+
|
|
316
|
+
vm.prank(owner);
|
|
317
|
+
if (amount > remaining) {
|
|
318
|
+
vm.expectRevert(abi.encodeWithSelector(ERC20Capped.ERC20ExceededCap.selector, INITIAL_MINT + amount, CAP));
|
|
319
|
+
}
|
|
320
|
+
token.mint(alice, amount);
|
|
321
|
+
|
|
322
|
+
assertLe(token.totalSupply(), CAP);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function testFuzz_BurnDoesNotUnderflow(uint256 amount) public {
|
|
326
|
+
amount = bound(amount, 1, INITIAL_MINT);
|
|
327
|
+
|
|
328
|
+
vm.prank(owner);
|
|
329
|
+
token.burn(amount);
|
|
330
|
+
|
|
331
|
+
assertEq(token.balanceOf(owner), INITIAL_MINT - amount);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/*//////////////////////////////////////////////////////////////
|
|
335
|
+
HELPERS
|
|
336
|
+
//////////////////////////////////////////////////////////////*/
|
|
337
|
+
|
|
338
|
+
function _permitDigest(
|
|
339
|
+
address ownerAddr,
|
|
340
|
+
address spender,
|
|
341
|
+
uint256 value,
|
|
342
|
+
uint256 nonce,
|
|
343
|
+
uint256 deadline
|
|
344
|
+
)
|
|
345
|
+
internal
|
|
346
|
+
view
|
|
347
|
+
returns (bytes32)
|
|
348
|
+
{
|
|
349
|
+
bytes32 PERMIT_TYPEHASH =
|
|
350
|
+
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
|
|
351
|
+
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, ownerAddr, spender, value, nonce, deadline));
|
|
352
|
+
return keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
|
20
|
+
|
|
21
|
+
# ---- ERC-721 Collection parametreleri ----
|
|
22
|
+
# Koleksiyon adı (opsiyonel)
|
|
23
|
+
NFT_NAME=
|
|
24
|
+
# Koleksiyon sembolü (opsiyonel)
|
|
25
|
+
NFT_SYMBOL=
|
|
26
|
+
# Base URI (ipfs://.../ ile bitmeli) (opsiyonel)
|
|
27
|
+
NFT_BASE_URI=
|
|
28
|
+
# Maksimum arz (opsiyonel)
|
|
29
|
+
NFT_MAX_SUPPLY=
|
|
30
|
+
# Mint fiyatı (wei) (opsiyonel)
|
|
31
|
+
NFT_MINT_PRICE=
|
|
32
|
+
# Royalty (basis points, 500 = %5) (opsiyonel)
|
|
33
|
+
NFT_ROYALTY_BPS=
|
|
34
|
+
# Royalty alıcı adresi (opsiyonel)
|
|
35
|
+
NFT_ROYALTY_RECEIVER=
|
|
36
|
+
# Sahip adresi (boş → deployer) (opsiyonel)
|
|
37
|
+
NFT_OWNER=
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# ERC-721 Collection
|
|
2
|
+
|
|
3
|
+
Merkle allowlist + faz bazlı mint, royalty destekli NFT koleksiyonu (KozaCollection).
|
|
4
|
+
|
|
5
|
+
Bu, kozalak-l1 deposundan üretilmiş **standalone** bir Foundry projesidir;
|
|
6
|
+
kendi başına derlenir ve test edilir.
|
|
7
|
+
|
|
8
|
+
## Kurulum
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
forge install foundry-rs/forge-std@8987040ede9553cea20c95ad40d0455930f9c8e0 OpenZeppelin/openzeppelin-contracts@e4f70216d759d8e6a64144a9e1f7bbeed78e7079
|
|
12
|
+
forge build
|
|
13
|
+
forge test
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> `forge install ...@<commit>` bağımlılıkları repo ile birebir aynı commit'lere
|
|
17
|
+
> pinler (aşağıdaki tabloya bakın). `.gitmodules` ve `remappings.txt`
|
|
18
|
+
> bu pinlerle uyumludur.
|
|
19
|
+
|
|
20
|
+
## Deploy (Fuji testnet)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cp .env.example .env # PRIVATE_KEY + SNOWTRACE_API_KEY doldur
|
|
24
|
+
forge script script/DeployERC721Collection.s.sol \
|
|
25
|
+
--rpc-url fuji \
|
|
26
|
+
--broadcast \
|
|
27
|
+
--verify
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> Production: `PRIVATE_KEY` yalnızca testnet olmalı; sahiplik/yönetici
|
|
31
|
+
> adreslerini bir multisig'e (Safe) yönlendir, EOA bırakma.
|
|
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
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
_Bu proje `create-kozalak-l1` tarafından üretildi. Kaynak: kozalak-l1 mono-repo._
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[profile.default]
|
|
2
|
+
src = "src"
|
|
3
|
+
out = "out"
|
|
4
|
+
libs = ["lib"]
|
|
5
|
+
test = "test"
|
|
6
|
+
script = "script"
|
|
7
|
+
# Bu şablon solc 0.8.34 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
|
+
]
|
|
19
|
+
|
|
20
|
+
[fmt]
|
|
21
|
+
line_length = 120
|
|
22
|
+
tab_width = 4
|
|
23
|
+
bracket_spacing = false
|
|
24
|
+
int_types = "long"
|
|
25
|
+
multiline_func_header = "all"
|
|
26
|
+
quote_style = "double"
|
|
27
|
+
number_underscore = "thousands"
|
|
28
|
+
|
|
29
|
+
[rpc_endpoints]
|
|
30
|
+
fuji = "https://api.avax-test.network/ext/bc/C/rpc"
|
|
31
|
+
avalanche = "https://api.avax.network/ext/bc/C/rpc"
|
|
32
|
+
|
|
33
|
+
[etherscan]
|
|
34
|
+
fuji = { key = "${SNOWTRACE_API_KEY}", url = "https://api.routescan.io/v2/network/testnet/evm/43113/etherscan", chain = 43113 }
|
|
35
|
+
avalanche = { key = "${SNOWTRACE_API_KEY}", url = "https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan", chain = 43114 }
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.34;
|
|
3
|
+
|
|
4
|
+
import {Script} from "forge-std/Script.sol";
|
|
5
|
+
import {console2} from "forge-std/console2.sol";
|
|
6
|
+
import {KozaCollection} from "../src/KozaCollection.sol";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @title DeployERC721Collection
|
|
10
|
+
* @notice Foundry deployment script for KozaCollection (Phase 1, Template 2).
|
|
11
|
+
*
|
|
12
|
+
* Usage (Fuji testnet, with .env populated):
|
|
13
|
+
*
|
|
14
|
+
* forge script script/deploy/DeployERC721Collection.s.sol \
|
|
15
|
+
* --rpc-url fuji \
|
|
16
|
+
* --broadcast \
|
|
17
|
+
* --verify
|
|
18
|
+
*
|
|
19
|
+
* Required env (loaded by `forge` from .env):
|
|
20
|
+
* - PRIVATE_KEY — deployer private key (testnet only!)
|
|
21
|
+
*
|
|
22
|
+
* Optional env (defaults below if unset):
|
|
23
|
+
* - NFT_NAME ("Koza Genesis")
|
|
24
|
+
* - NFT_SYMBOL ("KOZA")
|
|
25
|
+
* - NFT_BASE_URI ("ipfs://CHANGE_ME/") — sonu `/` ile bitir
|
|
26
|
+
* - NFT_MAX_SUPPLY (5000)
|
|
27
|
+
* - NFT_MINT_PRICE (0.05 ether)
|
|
28
|
+
* - NFT_ROYALTY_RECEIVER (broadcaster)
|
|
29
|
+
* - NFT_ROYALTY_BPS (500 = %5)
|
|
30
|
+
* - NFT_OWNER (broadcaster) — production: multisig
|
|
31
|
+
*
|
|
32
|
+
* Production checklist:
|
|
33
|
+
* - PRIVATE_KEY testnet-only veya cast-wallet/keystore ile değiştirilmeli
|
|
34
|
+
* - NFT_OWNER + NFT_ROYALTY_RECEIVER birer Safe (Gnosis) multisig olmalı
|
|
35
|
+
* - NFT_BASE_URI gerçek IPFS CID veya HTTPS gateway içermeli (sonu `/` ile)
|
|
36
|
+
* - Snowtrace/Routescan API key `.env`'de SNOWTRACE_API_KEY olarak set'li olmalı
|
|
37
|
+
*/
|
|
38
|
+
contract DeployERC721Collection is Script {
|
|
39
|
+
/*//////////////////////////////////////////////////////////////
|
|
40
|
+
DEFAULTS
|
|
41
|
+
//////////////////////////////////////////////////////////////*/
|
|
42
|
+
|
|
43
|
+
string internal constant DEFAULT_NAME = "Koza Genesis";
|
|
44
|
+
string internal constant DEFAULT_SYMBOL = "KOZA";
|
|
45
|
+
string internal constant DEFAULT_BASE_URI = "ipfs://CHANGE_ME/";
|
|
46
|
+
uint256 internal constant DEFAULT_MAX_SUPPLY = 5000;
|
|
47
|
+
uint256 internal constant DEFAULT_MINT_PRICE = 0.05 ether;
|
|
48
|
+
uint96 internal constant DEFAULT_ROYALTY_BPS = 500; // %5
|
|
49
|
+
|
|
50
|
+
/*//////////////////////////////////////////////////////////////
|
|
51
|
+
RUN
|
|
52
|
+
//////////////////////////////////////////////////////////////*/
|
|
53
|
+
|
|
54
|
+
/// @notice Entry point invoked by `forge script`. Reads parameters from env.
|
|
55
|
+
function run() external returns (KozaCollection nft, address deployer) {
|
|
56
|
+
string memory name = vm.envOr("NFT_NAME", DEFAULT_NAME);
|
|
57
|
+
string memory symbol = vm.envOr("NFT_SYMBOL", DEFAULT_SYMBOL);
|
|
58
|
+
string memory baseURI = vm.envOr("NFT_BASE_URI", DEFAULT_BASE_URI);
|
|
59
|
+
uint256 maxSupply = vm.envOr("NFT_MAX_SUPPLY", DEFAULT_MAX_SUPPLY);
|
|
60
|
+
uint256 mintPrice = vm.envOr("NFT_MINT_PRICE", DEFAULT_MINT_PRICE);
|
|
61
|
+
uint256 royaltyBpsRaw = vm.envOr("NFT_ROYALTY_BPS", uint256(DEFAULT_ROYALTY_BPS));
|
|
62
|
+
uint96 royaltyBps = uint96(royaltyBpsRaw);
|
|
63
|
+
|
|
64
|
+
address broadcaster = _resolveBroadcaster();
|
|
65
|
+
address royaltyReceiver = vm.envOr("NFT_ROYALTY_RECEIVER", broadcaster);
|
|
66
|
+
address owner = vm.envOr("NFT_OWNER", broadcaster);
|
|
67
|
+
|
|
68
|
+
return deploy(name, symbol, baseURI, maxSupply, mintPrice, royaltyReceiver, royaltyBps, owner, broadcaster);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// @notice Test-friendly entry point. Takes explicit parameters instead of env.
|
|
72
|
+
/// @dev Prefer this from Foundry tests so env state does not leak between cases.
|
|
73
|
+
function deploy(
|
|
74
|
+
string memory name,
|
|
75
|
+
string memory symbol,
|
|
76
|
+
string memory baseURI,
|
|
77
|
+
uint256 maxSupply,
|
|
78
|
+
uint256 mintPrice,
|
|
79
|
+
address royaltyReceiver,
|
|
80
|
+
uint96 royaltyBps,
|
|
81
|
+
address owner,
|
|
82
|
+
address broadcaster
|
|
83
|
+
)
|
|
84
|
+
public
|
|
85
|
+
returns (KozaCollection nft, address deployer)
|
|
86
|
+
{
|
|
87
|
+
_logPreDeploy(name, symbol, baseURI, maxSupply, mintPrice, royaltyReceiver, royaltyBps, owner, broadcaster);
|
|
88
|
+
|
|
89
|
+
vm.startBroadcast();
|
|
90
|
+
nft = new KozaCollection(name, symbol, baseURI, maxSupply, mintPrice, royaltyReceiver, royaltyBps, owner);
|
|
91
|
+
vm.stopBroadcast();
|
|
92
|
+
|
|
93
|
+
deployer = broadcaster;
|
|
94
|
+
|
|
95
|
+
_logPostDeploy(nft, owner);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*//////////////////////////////////////////////////////////////
|
|
99
|
+
INTERNALS
|
|
100
|
+
//////////////////////////////////////////////////////////////*/
|
|
101
|
+
|
|
102
|
+
/// @dev Order: explicit DEPLOYER_ADDRESS > derived from PRIVATE_KEY > tx.origin.
|
|
103
|
+
function _resolveBroadcaster() internal view returns (address) {
|
|
104
|
+
address explicitDeployer = vm.envOr("DEPLOYER_ADDRESS", address(0));
|
|
105
|
+
if (explicitDeployer != address(0)) return explicitDeployer;
|
|
106
|
+
|
|
107
|
+
uint256 pk = vm.envOr("PRIVATE_KEY", uint256(0));
|
|
108
|
+
if (pk != 0) return vm.addr(pk);
|
|
109
|
+
|
|
110
|
+
return tx.origin;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _logPreDeploy(
|
|
114
|
+
string memory name,
|
|
115
|
+
string memory symbol,
|
|
116
|
+
string memory baseURI,
|
|
117
|
+
uint256 maxSupply,
|
|
118
|
+
uint256 mintPrice,
|
|
119
|
+
address royaltyReceiver,
|
|
120
|
+
uint96 royaltyBps,
|
|
121
|
+
address owner,
|
|
122
|
+
address broadcaster
|
|
123
|
+
)
|
|
124
|
+
internal
|
|
125
|
+
pure
|
|
126
|
+
{
|
|
127
|
+
console2.log("=== Deploying KozaCollection ===");
|
|
128
|
+
console2.log(" Broadcaster: ", broadcaster);
|
|
129
|
+
console2.log(" Owner: ", owner);
|
|
130
|
+
console2.log(" Name: ", name);
|
|
131
|
+
console2.log(" Symbol: ", symbol);
|
|
132
|
+
console2.log(" Base URI: ", baseURI);
|
|
133
|
+
console2.log(" Max supply: ", maxSupply);
|
|
134
|
+
console2.log(" Mint price (wei):", mintPrice);
|
|
135
|
+
console2.log(" Royalty BPS: ", royaltyBps);
|
|
136
|
+
console2.log(" Royalty receiver:", royaltyReceiver);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _logPostDeploy(KozaCollection nft, address owner_) internal view {
|
|
140
|
+
console2.log("=== Deployed ===");
|
|
141
|
+
console2.log(" Address: ", address(nft));
|
|
142
|
+
console2.log(" Owner: ", nft.owner());
|
|
143
|
+
console2.log(" Owner (input): ", owner_);
|
|
144
|
+
console2.log(" Phase: ", uint8(nft.phase()));
|
|
145
|
+
console2.log("");
|
|
146
|
+
console2.log("Next steps:");
|
|
147
|
+
console2.log(" 1) cast send <addr> 'setMerkleRoot(bytes32)' <root> --rpc-url fuji ...");
|
|
148
|
+
console2.log(" 2) cast send <addr> 'setPhase(uint8)' 1 # Allowlist");
|
|
149
|
+
console2.log(" 3) cast send <addr> 'setPhase(uint8)' 2 # Public");
|
|
150
|
+
}
|
|
151
|
+
}
|