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,319 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.34;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {KozaCredential} from "../src/KozaCredential.sol";
6
+ import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
7
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
8
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
9
+ import {IERC721Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
10
+
11
+ /**
12
+ * @title SoulboundTest
13
+ * @notice Unit + fuzz tests for KozaCredential (Phase 1, Template 4).
14
+ * @dev Foundry suite covering constructor/roles, issue, soulbound transfer lock,
15
+ * revoke (flag), isValid, tokenURI, getCredential, AccessControl grant/revoke.
16
+ * Invariant tests are in Soulbound.invariants.t.sol.
17
+ */
18
+ contract SoulboundTest is Test {
19
+ /*//////////////////////////////////////////////////////////////
20
+ STATE
21
+ //////////////////////////////////////////////////////////////*/
22
+
23
+ KozaCredential internal cred;
24
+
25
+ address internal admin;
26
+ address internal issuer;
27
+ address internal alice;
28
+ address internal bob;
29
+
30
+ string internal constant NAME = "ARIA Hub Credential";
31
+ string internal constant SYMBOL = "ARIA";
32
+ string internal constant COURSE = "Avalanche L1 Workshop";
33
+
34
+ bytes32 internal ISSUER_ROLE;
35
+ bytes32 internal DEFAULT_ADMIN_ROLE;
36
+
37
+ /*//////////////////////////////////////////////////////////////
38
+ SETUP
39
+ //////////////////////////////////////////////////////////////*/
40
+
41
+ function setUp() public {
42
+ admin = makeAddr("admin");
43
+ issuer = makeAddr("issuer");
44
+ alice = makeAddr("alice");
45
+ bob = makeAddr("bob");
46
+
47
+ cred = new KozaCredential(NAME, SYMBOL, admin, issuer);
48
+ ISSUER_ROLE = cred.ISSUER_ROLE();
49
+ DEFAULT_ADMIN_ROLE = cred.DEFAULT_ADMIN_ROLE();
50
+ }
51
+
52
+ /*//////////////////////////////////////////////////////////////
53
+ CONSTRUCTOR
54
+ //////////////////////////////////////////////////////////////*/
55
+
56
+ function test_Constructor_SetsMetadataAndRoles() public view {
57
+ assertEq(cred.name(), NAME);
58
+ assertEq(cred.symbol(), SYMBOL);
59
+ assertEq(cred.totalIssued(), 0);
60
+ assertTrue(cred.hasRole(DEFAULT_ADMIN_ROLE, admin));
61
+ assertTrue(cred.hasRole(ISSUER_ROLE, issuer));
62
+ assertFalse(cred.hasRole(ISSUER_ROLE, alice));
63
+ }
64
+
65
+ function test_Constructor_SupportsInterfaces() public view {
66
+ assertTrue(cred.supportsInterface(type(IERC721).interfaceId));
67
+ assertTrue(cred.supportsInterface(type(IAccessControl).interfaceId));
68
+ assertTrue(cred.supportsInterface(type(IERC165).interfaceId));
69
+ }
70
+
71
+ /*//////////////////////////////////////////////////////////////
72
+ ISSUE
73
+ //////////////////////////////////////////////////////////////*/
74
+
75
+ function test_Issue_Succeeds() public {
76
+ vm.expectEmit(true, true, true, true);
77
+ emit KozaCredential.CredentialIssued(1, alice, issuer, COURSE);
78
+
79
+ vm.prank(issuer);
80
+ uint256 tokenId = cred.issue(alice, COURSE);
81
+
82
+ assertEq(tokenId, 1);
83
+ assertEq(cred.ownerOf(1), alice);
84
+ assertEq(cred.balanceOf(alice), 1);
85
+ assertEq(cred.totalIssued(), 1);
86
+ assertTrue(cred.isValid(1));
87
+
88
+ KozaCredential.Credential memory c = cred.getCredential(1);
89
+ assertEq(c.course, COURSE);
90
+ assertEq(c.issuer, issuer);
91
+ assertEq(c.issuedAt, uint64(block.timestamp));
92
+ assertFalse(c.revoked);
93
+ }
94
+
95
+ function test_Issue_IncrementsTokenId() public {
96
+ vm.startPrank(issuer);
97
+ assertEq(cred.issue(alice, COURSE), 1);
98
+ assertEq(cred.issue(bob, COURSE), 2);
99
+ assertEq(cred.issue(alice, "Second Course"), 3);
100
+ vm.stopPrank();
101
+
102
+ assertEq(cred.totalIssued(), 3);
103
+ assertEq(cred.balanceOf(alice), 2);
104
+ }
105
+
106
+ function test_Issue_RecordsTimestamp() public {
107
+ vm.warp(1_900_000_000);
108
+ vm.prank(issuer);
109
+ cred.issue(alice, COURSE);
110
+ assertEq(cred.getCredential(1).issuedAt, uint64(1_900_000_000));
111
+ }
112
+
113
+ function test_RevertWhen_IssueNotIssuer() public {
114
+ vm.expectRevert(
115
+ abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, alice, ISSUER_ROLE)
116
+ );
117
+ vm.prank(alice);
118
+ cred.issue(bob, COURSE);
119
+ }
120
+
121
+ function test_RevertWhen_IssueEmptyCourse() public {
122
+ vm.expectRevert(KozaCredential.EmptyCourse.selector);
123
+ vm.prank(issuer);
124
+ cred.issue(alice, "");
125
+ }
126
+
127
+ /*//////////////////////////////////////////////////////////////
128
+ SOULBOUND (NON-TRANSFER)
129
+ //////////////////////////////////////////////////////////////*/
130
+
131
+ function test_RevertWhen_TransferFrom() public {
132
+ vm.prank(issuer);
133
+ cred.issue(alice, COURSE);
134
+
135
+ vm.expectRevert(KozaCredential.Soulbound.selector);
136
+ vm.prank(alice);
137
+ cred.transferFrom(alice, bob, 1);
138
+ }
139
+
140
+ function test_RevertWhen_SafeTransferFrom() public {
141
+ vm.prank(issuer);
142
+ cred.issue(alice, COURSE);
143
+
144
+ vm.expectRevert(KozaCredential.Soulbound.selector);
145
+ vm.prank(alice);
146
+ cred.safeTransferFrom(alice, bob, 1);
147
+ }
148
+
149
+ function test_RevertWhen_TransferAfterApprove() public {
150
+ vm.prank(issuer);
151
+ cred.issue(alice, COURSE);
152
+
153
+ // approve is allowed (no-op effectively) but transfer still blocked
154
+ vm.prank(alice);
155
+ cred.approve(bob, 1);
156
+
157
+ vm.expectRevert(KozaCredential.Soulbound.selector);
158
+ vm.prank(bob);
159
+ cred.transferFrom(alice, bob, 1);
160
+ }
161
+
162
+ /*//////////////////////////////////////////////////////////////
163
+ REVOKE
164
+ //////////////////////////////////////////////////////////////*/
165
+
166
+ function test_Revoke_Succeeds() public {
167
+ vm.prank(issuer);
168
+ cred.issue(alice, COURSE);
169
+
170
+ vm.expectEmit(true, true, false, false);
171
+ emit KozaCredential.CredentialRevoked(1, issuer);
172
+ vm.prank(issuer);
173
+ cred.revoke(1);
174
+
175
+ assertFalse(cred.isValid(1));
176
+ assertTrue(cred.getCredential(1).revoked);
177
+ // token is not burned — ownerOf still resolves
178
+ assertEq(cred.ownerOf(1), alice);
179
+ }
180
+
181
+ function test_RevertWhen_RevokeNotIssuer() public {
182
+ vm.prank(issuer);
183
+ cred.issue(alice, COURSE);
184
+
185
+ vm.expectRevert(
186
+ abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, alice, ISSUER_ROLE)
187
+ );
188
+ vm.prank(alice);
189
+ cred.revoke(1);
190
+ }
191
+
192
+ function test_RevertWhen_RevokeNonexistent() public {
193
+ vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, 99));
194
+ vm.prank(issuer);
195
+ cred.revoke(99);
196
+ }
197
+
198
+ function test_RevertWhen_RevokeAlreadyRevoked() public {
199
+ vm.startPrank(issuer);
200
+ cred.issue(alice, COURSE);
201
+ cred.revoke(1);
202
+ vm.expectRevert(abi.encodeWithSelector(KozaCredential.AlreadyRevoked.selector, 1));
203
+ cred.revoke(1);
204
+ vm.stopPrank();
205
+ }
206
+
207
+ /*//////////////////////////////////////////////////////////////
208
+ IS VALID
209
+ //////////////////////////////////////////////////////////////*/
210
+
211
+ function test_IsValid_FalseForNonexistent() public view {
212
+ assertFalse(cred.isValid(1));
213
+ }
214
+
215
+ /*//////////////////////////////////////////////////////////////
216
+ TOKEN URI
217
+ //////////////////////////////////////////////////////////////*/
218
+
219
+ function test_TokenURI_IsOnChainJson() public {
220
+ vm.prank(issuer);
221
+ cred.issue(alice, COURSE);
222
+
223
+ string memory uri = cred.tokenURI(1);
224
+ assertTrue(_startsWith(uri, "data:application/json;base64,"), "tokenURI must be on-chain base64 JSON");
225
+ }
226
+
227
+ function test_TokenURI_ChangesAfterRevoke() public {
228
+ vm.prank(issuer);
229
+ cred.issue(alice, COURSE);
230
+ string memory beforeURI = cred.tokenURI(1);
231
+
232
+ vm.prank(issuer);
233
+ cred.revoke(1);
234
+ string memory afterURI = cred.tokenURI(1);
235
+
236
+ // status flips Valid -> Revoked, so the encoded metadata must differ
237
+ assertTrue(keccak256(bytes(beforeURI)) != keccak256(bytes(afterURI)), "metadata must reflect revoke");
238
+ }
239
+
240
+ function test_RevertWhen_TokenURINonexistent() public {
241
+ vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, 1));
242
+ cred.tokenURI(1);
243
+ }
244
+
245
+ /*//////////////////////////////////////////////////////////////
246
+ ACCESS CONTROL
247
+ //////////////////////////////////////////////////////////////*/
248
+
249
+ function test_AdminCanGrantIssuerRole() public {
250
+ vm.prank(admin);
251
+ cred.grantRole(ISSUER_ROLE, bob);
252
+
253
+ vm.prank(bob);
254
+ uint256 tokenId = cred.issue(alice, COURSE);
255
+ assertEq(tokenId, 1);
256
+ }
257
+
258
+ function test_AdminCanRevokeIssuerRole() public {
259
+ vm.prank(admin);
260
+ cred.revokeRole(ISSUER_ROLE, issuer);
261
+
262
+ vm.expectRevert(
263
+ abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, issuer, ISSUER_ROLE)
264
+ );
265
+ vm.prank(issuer);
266
+ cred.issue(alice, COURSE);
267
+ }
268
+
269
+ function test_RevertWhen_NonAdminGrantsRole() public {
270
+ vm.expectRevert(
271
+ abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, alice, DEFAULT_ADMIN_ROLE)
272
+ );
273
+ vm.prank(alice);
274
+ cred.grantRole(ISSUER_ROLE, bob);
275
+ }
276
+
277
+ /*//////////////////////////////////////////////////////////////
278
+ FUZZ
279
+ //////////////////////////////////////////////////////////////*/
280
+
281
+ function testFuzz_IssuedCredentialsAreSoulbound(address to, address attacker) public {
282
+ vm.assume(to != address(0) && to.code.length == 0);
283
+ vm.assume(attacker != address(0));
284
+
285
+ vm.prank(issuer);
286
+ uint256 tokenId = cred.issue(to, COURSE);
287
+
288
+ vm.expectRevert(KozaCredential.Soulbound.selector);
289
+ vm.prank(to);
290
+ cred.transferFrom(to, attacker, tokenId);
291
+ }
292
+
293
+ function testFuzz_RevokeInvalidatesCredential(uint64 ts) public {
294
+ ts = uint64(bound(ts, 1, type(uint64).max));
295
+ vm.warp(ts);
296
+
297
+ vm.startPrank(issuer);
298
+ cred.issue(alice, COURSE);
299
+ assertTrue(cred.isValid(1));
300
+ cred.revoke(1);
301
+ vm.stopPrank();
302
+
303
+ assertFalse(cred.isValid(1));
304
+ }
305
+
306
+ /*//////////////////////////////////////////////////////////////
307
+ HELPERS
308
+ //////////////////////////////////////////////////////////////*/
309
+
310
+ function _startsWith(string memory str, string memory prefix) internal pure returns (bool) {
311
+ bytes memory s = bytes(str);
312
+ bytes memory p = bytes(prefix);
313
+ if (s.length < p.length) return false;
314
+ for (uint256 i = 0; i < p.length; i++) {
315
+ if (s[i] != p[i]) return false;
316
+ }
317
+ return true;
318
+ }
319
+ }
@@ -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,6 @@
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
@@ -0,0 +1,48 @@
1
+ # Treasury Multisig (Timelock)
2
+
3
+ OpenZeppelin TimelockController tabanlı, gecikmeli-yürütme hazine kontratı (KozaTreasury).
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/DeployTreasury.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,5 @@
1
+ .env
2
+ out/
3
+ cache/
4
+ broadcast/
5
+ lib/
@@ -0,0 +1,2 @@
1
+ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
2
+ forge-std/=lib/forge-std/src/
@@ -0,0 +1,128 @@
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 {KozaTreasury} from "../src/KozaTreasury.sol";
7
+
8
+ /**
9
+ * @title DeployTreasury
10
+ * @notice Foundry deployment script for KozaTreasury (Phase 1, Template 5).
11
+ *
12
+ * Usage (Fuji testnet, with .env populated):
13
+ *
14
+ * forge script script/deploy/DeployTreasury.s.sol:DeployTreasury \
15
+ * --rpc-url fuji --broadcast --verify
16
+ *
17
+ * Required env:
18
+ * - PRIVATE_KEY — deployer private key (testnet only!)
19
+ *
20
+ * Optional env (defaults below if unset):
21
+ * - TREASURY_MIN_DELAY (172800 = 48h)
22
+ * - TREASURY_PROPOSER (broadcaster) — production: Safe multisig
23
+ * - TREASURY_EXECUTOR (broadcaster) — `address(0)` → açık execute
24
+ * - TREASURY_ADMIN (broadcaster) — production: address(0) (self-administered)
25
+ *
26
+ * Production checklist:
27
+ * - TREASURY_MIN_DELAY kritik hazine için 48h+ olmalı
28
+ * - TREASURY_PROPOSER / TREASURY_EXECUTOR birer Safe (Gnosis) multisig olmalı
29
+ * - TREASURY_ADMIN = address(0) (self-administered) en güvenli; deployer verilirse
30
+ * kurulum sonrası renounceRole(DEFAULT_ADMIN_ROLE, deployer)
31
+ * - Korunan kontratların ownership'i bu timelock'a devredilmeli
32
+ */
33
+ contract DeployTreasury is Script {
34
+ /*//////////////////////////////////////////////////////////////
35
+ DEFAULTS
36
+ //////////////////////////////////////////////////////////////*/
37
+
38
+ uint256 internal constant DEFAULT_MIN_DELAY = 48 hours; // 172800 s
39
+
40
+ /*//////////////////////////////////////////////////////////////
41
+ RUN
42
+ //////////////////////////////////////////////////////////////*/
43
+
44
+ /// @notice Entry point invoked by `forge script`. Reads parameters from env.
45
+ function run() external returns (KozaTreasury treasury, address deployer) {
46
+ uint256 minDelay = vm.envOr("TREASURY_MIN_DELAY", DEFAULT_MIN_DELAY);
47
+
48
+ address broadcaster = _resolveBroadcaster();
49
+ address proposer = vm.envOr("TREASURY_PROPOSER", broadcaster);
50
+ address executor = vm.envOr("TREASURY_EXECUTOR", broadcaster);
51
+ address admin = vm.envOr("TREASURY_ADMIN", broadcaster);
52
+
53
+ address[] memory proposers = new address[](1);
54
+ proposers[0] = proposer;
55
+ address[] memory executors = new address[](1);
56
+ executors[0] = executor;
57
+
58
+ return deploy(minDelay, proposers, executors, admin, broadcaster);
59
+ }
60
+
61
+ /// @notice Test-friendly entry point. Takes explicit parameters instead of env.
62
+ function deploy(
63
+ uint256 minDelay,
64
+ address[] memory proposers,
65
+ address[] memory executors,
66
+ address admin,
67
+ address broadcaster
68
+ )
69
+ public
70
+ returns (KozaTreasury treasury, address deployer)
71
+ {
72
+ _logPreDeploy(minDelay, proposers, executors, admin, broadcaster);
73
+
74
+ vm.startBroadcast();
75
+ treasury = new KozaTreasury(minDelay, proposers, executors, admin);
76
+ vm.stopBroadcast();
77
+
78
+ deployer = broadcaster;
79
+
80
+ _logPostDeploy(treasury);
81
+ }
82
+
83
+ /*//////////////////////////////////////////////////////////////
84
+ INTERNALS
85
+ //////////////////////////////////////////////////////////////*/
86
+
87
+ /// @dev Order: explicit DEPLOYER_ADDRESS > derived from PRIVATE_KEY > tx.origin.
88
+ function _resolveBroadcaster() internal view returns (address) {
89
+ address explicitDeployer = vm.envOr("DEPLOYER_ADDRESS", address(0));
90
+ if (explicitDeployer != address(0)) return explicitDeployer;
91
+
92
+ uint256 pk = vm.envOr("PRIVATE_KEY", uint256(0));
93
+ if (pk != 0) return vm.addr(pk);
94
+
95
+ return tx.origin;
96
+ }
97
+
98
+ function _logPreDeploy(
99
+ uint256 minDelay,
100
+ address[] memory proposers,
101
+ address[] memory executors,
102
+ address admin,
103
+ address broadcaster
104
+ )
105
+ internal
106
+ pure
107
+ {
108
+ console2.log("=== Deploying KozaTreasury ===");
109
+ console2.log(" Broadcaster: ", broadcaster);
110
+ console2.log(" Min delay (s):", minDelay);
111
+ console2.log(" Proposer[0]: ", proposers[0]);
112
+ console2.log(" Executor[0]: ", executors[0]);
113
+ console2.log(" Admin: ", admin);
114
+ }
115
+
116
+ function _logPostDeploy(KozaTreasury treasury) internal view {
117
+ console2.log("=== Deployed ===");
118
+ console2.log(" Address: ", address(treasury));
119
+ console2.log(" Min delay: ", treasury.getMinDelay());
120
+ console2.log("");
121
+ console2.log("Next steps:");
122
+ console2.log(" 1) Fonla: native gonder veya korunan kontrat ownership'ini timelock'a devret");
123
+ console2.log(" 2) proposer: cast send <addr> 'schedule(address,uint256,bytes,bytes32,bytes32,uint256)' ...");
124
+ console2.log(
125
+ " 3) minDelay sonra executor: cast send <addr> 'execute(address,uint256,bytes,bytes32,bytes32)' ..."
126
+ );
127
+ }
128
+ }
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.34;
3
+
4
+ import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
5
+
6
+ /**
7
+ * @title KozaTreasury
8
+ * @author kozalak-L1 contributors
9
+ * @notice Zaman-kilitli (timelock) hazine — OpenZeppelin `TimelockController` ince wrapper'ı.
10
+ * DAO/topluluk fonlarını rol-bazlı + gecikmeli yürütmeyle korur. Avalanche L1 /
11
+ * C-Chain için audit-grade boilerplate.
12
+ * @dev OZ v5.3+ `TimelockController`'ı doğrudan miras alır; custom logic eklemez. Audit-grade
13
+ * prensibi: minimum custom layer on top of audited primitives.
14
+ *
15
+ * "Multisig" katmanı: bir Safe (Gnosis) multisig'i `proposers`/`executors` olarak atanır.
16
+ * Öneri Safe'te imzalanır → timelock gecikmesi dolar → execute. Tek nokta güven yoktur;
17
+ * gecikme, kötü niyetli/yanlış önerilere tepki penceresi sağlar.
18
+ *
19
+ * Parent `TimelockController`'ın sağladıkları:
20
+ * - Roller: PROPOSER_ROLE (schedule), EXECUTOR_ROLE (execute), CANCELLER_ROLE (cancel),
21
+ * DEFAULT_ADMIN_ROLE (rol yönetimi).
22
+ * - `schedule`/`scheduleBatch` → minDelay bekle → `execute`/`executeBatch`.
23
+ * - `cancel` (henüz yürütülmemiş öneriyi iptal).
24
+ * - `updateDelay` — YALNIZCA timelock'un kendi önerisiyle değiştirilebilir.
25
+ * - `receive()` payable — native fon tutar; ERC20'ler `schedule(token, 0, transferCalldata, ...)`
26
+ * ile zaman-kilitli gönderilir.
27
+ * - `executor = address(0)` → açık execute (herkes yürütebilir, schedule yine korumalı).
28
+ *
29
+ * Production hazırlık checklist'i:
30
+ * - `minDelay` kritik hazine için anlamlı olmalı (örn. 48h+). Çok düşük delay timelock'un
31
+ * güvenlik değerini düşürür.
32
+ * - `proposers`/`executors` birer Safe multisig olmalı; EOA mainnet'te tek nokta risktir.
33
+ * - `admin = address(0)` (self-administered) en güvenlidir: roller yalnız timelock'un kendi
34
+ * gecikmeli önerisiyle değişir. Kurulum kolaylığı için deployer verilirse, kurulum
35
+ * sonrası `renounceRole(DEFAULT_ADMIN_ROLE, deployer)` ile bırakılması önerilir.
36
+ * - Hazine sahipliği: korunan kontratların `owner`/admin'i bu timelock olmalı.
37
+ *
38
+ * @custom:security-contact security@bekirerdem.dev
39
+ */
40
+ contract KozaTreasury is TimelockController {
41
+ /**
42
+ * @param minDelay Önerinin yürütülebilmesi için geçmesi gereken minimum süre (saniye)
43
+ * @param proposers PROPOSER_ROLE + CANCELLER_ROLE alacak adresler (production: Safe multisig)
44
+ * @param executors EXECUTOR_ROLE alacak adresler (`address(0)` → açık execute)
45
+ * @param admin DEFAULT_ADMIN_ROLE (production: `address(0)` self-administered önerilir)
46
+ */
47
+ constructor(
48
+ uint256 minDelay,
49
+ address[] memory proposers,
50
+ address[] memory executors,
51
+ address admin
52
+ )
53
+ TimelockController(minDelay, proposers, executors, admin)
54
+ {}
55
+ }