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,201 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.34;
3
+
4
+ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
5
+ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
6
+ import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
7
+ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
8
+
9
+ /**
10
+ * @title KozaCredential
11
+ * @author kozalak-L1 contributors
12
+ * @notice Account-bound (soulbound) sertifika NFT — transfer edilemez, issuer-only mint,
13
+ * on-chain metadata, revoke-flag. Avalanche L1 / C-Chain için audit-grade boilerplate.
14
+ * @dev OpenZeppelin v5.3+ pattern'leri. Token id'ler 1'den başlar (UX/explorer kolaylığı).
15
+ * Use-case: eğitim/topluluk platformlarının (örn. ARIA Hub) mezun/üye sertifikası.
16
+ *
17
+ * Neden ERC-5114 değil: ERC-5114 rozeti bir NFT'ye kalıcı bağlar (cüzdana değil) ve
18
+ * revoke/transfer/burn'e izin vermez. Eğitim sertifikası "kişiye verilir + issuer
19
+ * revoke edebilmeli" gereksinimine account-bound ERC-721 + revoke-flag oturur.
20
+ *
21
+ * Güvenlik kararları:
22
+ * - Soulbound: `_update` override ile transfer (from≠0 && to≠0) revert eder. Sadece
23
+ * mint izinli; `approve`/`setApprovalForAll` çağrılabilir ama transfer yine bloklu,
24
+ * dolayısıyla fiilen etkisizdir. Token cüzdana kilitlidir.
25
+ * - Revoke-flag (burn değil): iptal edilen sertifika silinmez, `revoked=true`
26
+ * işaretlenir; `ownerOf` çözülmeye devam eder. "Sertifika vardı, iptal edildi"
27
+ * on-chain denetlenebilir kalır. `isValid` ve metadata `Status`'a yansır.
28
+ * - AccessControl: ISSUER_ROLE (issue + revoke) ile DEFAULT_ADMIN_ROLE (rol yönetimi)
29
+ * ayrıdır. Production: admin = Safe (Gnosis) multisig, asla EOA değil. Çoklu
30
+ * eğitmen/issuer admin tarafından `grantRole(ISSUER_ROLE, ...)` ile eklenir.
31
+ * - On-chain metadata: kurs/issuer/tarih/durum zincirde saklanır, `tokenURI` base64
32
+ * JSON üretir. IPFS/sunucu ölse bile sertifikanın kanıt değeri kaybolmaz.
33
+ * - Minimum custom logic: audited OZ ERC721 + AccessControl primitive'leri üstüne
34
+ * ince katman. CEI: `issue` state'i `_safeMint`'ten ÖNCE yazar.
35
+ * - `course` metni JSON'a düz gömülür; issuer güvenilir aktördür (rol-korumalı).
36
+ * Untrusted girdi senaryosunda `"` escape'i gerekir — bu template issuer-trusted varsayar.
37
+ */
38
+ contract KozaCredential is ERC721, AccessControl {
39
+ using Strings for uint256;
40
+
41
+ /*//////////////////////////////////////////////////////////////
42
+ TYPES
43
+ //////////////////////////////////////////////////////////////*/
44
+
45
+ /// @notice Tek bir sertifikanın on-chain kaydı.
46
+ struct Credential {
47
+ string course; // sertifikanın konusu (kurs/etkinlik adı)
48
+ address issuer; // mint anında ISSUER_ROLE sahibi çağıran
49
+ uint64 issuedAt; // mint block.timestamp
50
+ bool revoked; // issuer tarafından iptal edildi mi
51
+ }
52
+
53
+ /*//////////////////////////////////////////////////////////////
54
+ CONSTANTS
55
+ //////////////////////////////////////////////////////////////*/
56
+
57
+ /// @notice Sertifika mint + revoke yetkisi olan rol.
58
+ bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE");
59
+
60
+ /*//////////////////////////////////////////////////////////////
61
+ STORAGE
62
+ //////////////////////////////////////////////////////////////*/
63
+
64
+ /// @notice Şimdiye kadar verilen toplam sertifika (id artışı için; id'ler 1'den başlar).
65
+ uint256 public totalIssued;
66
+
67
+ /// @dev tokenId → on-chain credential kaydı. Dış erişim `getCredential` ile.
68
+ mapping(uint256 tokenId => Credential) private _credentials;
69
+
70
+ /*//////////////////////////////////////////////////////////////
71
+ EVENTS
72
+ //////////////////////////////////////////////////////////////*/
73
+
74
+ event CredentialIssued(uint256 indexed tokenId, address indexed to, address indexed issuer, string course);
75
+ event CredentialRevoked(uint256 indexed tokenId, address indexed issuer);
76
+
77
+ /*//////////////////////////////////////////////////////////////
78
+ ERRORS
79
+ //////////////////////////////////////////////////////////////*/
80
+
81
+ /// @notice Sertifika transfer edilemez (account-bound).
82
+ error Soulbound();
83
+ /// @notice `course` boş olamaz.
84
+ error EmptyCourse();
85
+ /// @notice Sertifika zaten iptal edilmiş.
86
+ error AlreadyRevoked(uint256 tokenId);
87
+
88
+ /*//////////////////////////////////////////////////////////////
89
+ CONSTRUCTOR
90
+ //////////////////////////////////////////////////////////////*/
91
+
92
+ /**
93
+ * @param name_ Sertifika koleksiyonu adı (örn. "ARIA Hub Credential")
94
+ * @param symbol_ Sembol (örn. "ARIA")
95
+ * @param admin DEFAULT_ADMIN_ROLE sahibi — rol yönetimi (production: multisig)
96
+ * @param issuer_ İlk ISSUER_ROLE sahibi — sertifika verir/iptal eder
97
+ */
98
+ constructor(string memory name_, string memory symbol_, address admin, address issuer_) ERC721(name_, symbol_) {
99
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
100
+ _grantRole(ISSUER_ROLE, issuer_);
101
+ }
102
+
103
+ /*//////////////////////////////////////////////////////////////
104
+ ISSUER ACTIONS
105
+ //////////////////////////////////////////////////////////////*/
106
+
107
+ /**
108
+ * @notice Bir adrese soulbound sertifika ver.
109
+ * @param to Sertifikayı alacak cüzdan
110
+ * @param course Sertifikanın konusu (boş olamaz)
111
+ * @return tokenId Verilen sertifikanın id'si (1'den başlar)
112
+ */
113
+ function issue(address to, string calldata course) external onlyRole(ISSUER_ROLE) returns (uint256 tokenId) {
114
+ if (bytes(course).length == 0) revert EmptyCourse();
115
+
116
+ // CEI: state güncellemeleri _safeMint'ten (external call) ÖNCE yapılır.
117
+ tokenId = ++totalIssued;
118
+ _credentials[tokenId] =
119
+ Credential({course: course, issuer: msg.sender, issuedAt: uint64(block.timestamp), revoked: false});
120
+
121
+ emit CredentialIssued(tokenId, to, msg.sender, course);
122
+ _safeMint(to, tokenId);
123
+ }
124
+
125
+ /**
126
+ * @notice Verilmiş bir sertifikayı iptal et (token silinmez, `revoked` işaretlenir).
127
+ * @param tokenId İptal edilecek sertifika
128
+ */
129
+ function revoke(uint256 tokenId) external onlyRole(ISSUER_ROLE) {
130
+ _requireOwned(tokenId); // yoksa ERC721NonexistentToken
131
+ if (_credentials[tokenId].revoked) revert AlreadyRevoked(tokenId);
132
+
133
+ _credentials[tokenId].revoked = true;
134
+ emit CredentialRevoked(tokenId, msg.sender);
135
+ }
136
+
137
+ /*//////////////////////////////////////////////////////////////
138
+ VIEWS
139
+ //////////////////////////////////////////////////////////////*/
140
+
141
+ /// @notice Sertifika geçerli mi (mevcut ve iptal edilmemiş).
142
+ function isValid(uint256 tokenId) external view returns (bool) {
143
+ return _ownerOf(tokenId) != address(0) && !_credentials[tokenId].revoked;
144
+ }
145
+
146
+ /// @notice Sertifikanın on-chain kaydını döndür.
147
+ function getCredential(uint256 tokenId) external view returns (Credential memory) {
148
+ return _credentials[tokenId];
149
+ }
150
+
151
+ /**
152
+ * @notice On-chain üretilen base64 JSON metadata (IPFS bağımsız).
153
+ * @dev Token yoksa ERC721NonexistentToken revert eder (`_requireOwned`).
154
+ */
155
+ function tokenURI(uint256 tokenId) public view override returns (string memory) {
156
+ _requireOwned(tokenId);
157
+ Credential storage c = _credentials[tokenId];
158
+
159
+ string memory json = string.concat(
160
+ '{"name":"',
161
+ name(),
162
+ " #",
163
+ tokenId.toString(),
164
+ '","description":"Soulbound, transfer edilemez on-chain sertifika (Kozalak-L1 Template 4).",',
165
+ '"attributes":[',
166
+ '{"trait_type":"Course","value":"',
167
+ c.course,
168
+ '"},',
169
+ '{"trait_type":"Issuer","value":"',
170
+ Strings.toHexString(c.issuer),
171
+ '"},',
172
+ '{"trait_type":"Issued","display_type":"date","value":',
173
+ uint256(c.issuedAt).toString(),
174
+ "},",
175
+ '{"trait_type":"Status","value":"',
176
+ c.revoked ? "Revoked" : "Valid",
177
+ '"}]}'
178
+ );
179
+
180
+ return string.concat("data:application/json;base64,", Base64.encode(bytes(json)));
181
+ }
182
+
183
+ /*//////////////////////////////////////////////////////////////
184
+ INTERNAL
185
+ //////////////////////////////////////////////////////////////*/
186
+
187
+ /**
188
+ * @dev Soulbound kilidi: transfer (from≠0 && to≠0) yasak. Mint (from=0) izinli.
189
+ * Burn bu template'te kullanılmaz (revoke flag tercih edildi).
190
+ */
191
+ function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
192
+ address from = _ownerOf(tokenId);
193
+ if (from != address(0) && to != address(0)) revert Soulbound();
194
+ return super._update(to, tokenId, auth);
195
+ }
196
+
197
+ /// @notice Birden fazla interface (ERC721, AccessControl) desteklendiği için override.
198
+ function supportsInterface(bytes4 interfaceId) public view override(ERC721, AccessControl) returns (bool) {
199
+ return super.supportsInterface(interfaceId);
200
+ }
201
+ }
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.34;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {DeployCredential} from "../script/DeployCredential.s.sol";
6
+ import {KozaCredential} from "../src/KozaCredential.sol";
7
+
8
+ /**
9
+ * @title DeployCredentialTest
10
+ * @notice Smoke tests for the Soulbound deploy script. Calls `deploy(...)` directly
11
+ * with explicit parameters; env-driven `run()` exercised on Fuji integration.
12
+ */
13
+ contract DeployCredentialTest is Test {
14
+ bytes32 internal constant ISSUER_ROLE = keccak256("ISSUER_ROLE");
15
+ bytes32 internal constant ADMIN_ROLE = 0x00; // DEFAULT_ADMIN_ROLE
16
+
17
+ function test_Deploy_DefaultRolesToBroadcaster() public {
18
+ DeployCredential deployer = new DeployCredential();
19
+ address broadcaster = makeAddr("broadcaster");
20
+
21
+ (KozaCredential cred, address returnedDeployer) =
22
+ deployer.deploy("ARIA Hub Credential", "ARIA", broadcaster, broadcaster, broadcaster);
23
+
24
+ assertEq(cred.name(), "ARIA Hub Credential");
25
+ assertEq(cred.symbol(), "ARIA");
26
+ assertEq(cred.totalIssued(), 0);
27
+ assertTrue(cred.hasRole(ADMIN_ROLE, broadcaster));
28
+ assertTrue(cred.hasRole(ISSUER_ROLE, broadcaster));
29
+ assertEq(returnedDeployer, broadcaster);
30
+ }
31
+
32
+ function test_Deploy_SeparateAdminAndIssuer() public {
33
+ DeployCredential deployer = new DeployCredential();
34
+ address admin = makeAddr("admin");
35
+ address issuer = makeAddr("issuer");
36
+ address broadcaster = makeAddr("broadcaster");
37
+
38
+ (KozaCredential cred,) = deployer.deploy("Custom Cred", "CC", admin, issuer, broadcaster);
39
+
40
+ assertEq(cred.name(), "Custom Cred");
41
+ assertTrue(cred.hasRole(ADMIN_ROLE, admin));
42
+ assertTrue(cred.hasRole(ISSUER_ROLE, issuer));
43
+ assertFalse(cred.hasRole(ISSUER_ROLE, broadcaster));
44
+ assertFalse(cred.hasRole(ADMIN_ROLE, issuer));
45
+ }
46
+ }
@@ -0,0 +1,133 @@
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
+
7
+ /**
8
+ * @title SoulboundHandler
9
+ * @notice Foundry invariant testing handler. Bounded random calls into KozaCredential.
10
+ * @dev Issues, revokes ve transfer denemeleri yapar. Transfer her zaman revert eder
11
+ * (soulbound); handler bunu yutar. Invariant runner her tx sonrası kontrol eder.
12
+ */
13
+ contract SoulboundHandler is Test {
14
+ KozaCredential public cred;
15
+ address public issuer;
16
+ address[] public actors;
17
+
18
+ uint256 public callCount;
19
+ uint256 public successfulIssues;
20
+ uint256[] public revokedIds;
21
+
22
+ string internal constant COURSE = "Avalanche L1 Workshop";
23
+
24
+ constructor(KozaCredential _cred, address _issuer, address[] memory _actors) {
25
+ cred = _cred;
26
+ issuer = _issuer;
27
+ actors = _actors;
28
+ }
29
+
30
+ function _pickActor(uint256 seed) internal view returns (address) {
31
+ return actors[seed % actors.length];
32
+ }
33
+
34
+ /// @notice Issuer bir aktöre sertifika verir.
35
+ function issue(uint256 actorSeed) external {
36
+ callCount++;
37
+ address to = _pickActor(actorSeed);
38
+ vm.prank(issuer);
39
+ try cred.issue(to, COURSE) returns (uint256) {
40
+ successfulIssues++;
41
+ } catch {}
42
+ }
43
+
44
+ /// @notice Issuer rastgele bir mevcut sertifikayı iptal eder.
45
+ function revoke(uint256 idSeed) external {
46
+ callCount++;
47
+ uint256 total = cred.totalIssued();
48
+ if (total == 0) return;
49
+ uint256 id = (idSeed % total) + 1;
50
+ vm.prank(issuer);
51
+ try cred.revoke(id) {
52
+ revokedIds.push(id);
53
+ } catch {}
54
+ }
55
+
56
+ /// @notice Transfer denemesi — soulbound olduğu için DAİMA revert eder.
57
+ function tryTransfer(uint256 toSeed, uint256 idSeed) external {
58
+ callCount++;
59
+ uint256 total = cred.totalIssued();
60
+ if (total == 0) return;
61
+ uint256 id = (idSeed % total) + 1;
62
+ address owner = cred.ownerOf(id); // revoke token'ı silmez → her zaman çözülür
63
+ address to = _pickActor(toSeed);
64
+ vm.prank(owner);
65
+ try cred.transferFrom(owner, to, id) {} catch {}
66
+ }
67
+
68
+ function revokedCount() external view returns (uint256) {
69
+ return revokedIds.length;
70
+ }
71
+
72
+ function revokedIdAt(uint256 i) external view returns (uint256) {
73
+ return revokedIds[i];
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @title SoulboundInvariantTest
79
+ * @notice Stateful fuzzing invariants for KozaCredential.
80
+ *
81
+ * Tested invariants:
82
+ * 1. sum(balanceOf(actors)) == totalIssued (transfer imkansız, token leak yok)
83
+ * 2. revoke edilen her sertifika asla isValid dönmez
84
+ * 3. totalIssued == başarılı issue sayısı (monotonik, kayıpsız)
85
+ */
86
+ contract SoulboundInvariantTest is Test {
87
+ KozaCredential internal cred;
88
+ SoulboundHandler internal handler;
89
+ address internal admin;
90
+ address internal issuer;
91
+ address[] internal actors;
92
+
93
+ function setUp() public {
94
+ admin = makeAddr("admin");
95
+ issuer = makeAddr("issuer");
96
+
97
+ actors.push(makeAddr("alice"));
98
+ actors.push(makeAddr("bob"));
99
+ actors.push(makeAddr("charlie"));
100
+ actors.push(makeAddr("dave"));
101
+ actors.push(makeAddr("eve"));
102
+
103
+ cred = new KozaCredential("ARIA Hub Credential", "ARIA", admin, issuer);
104
+ handler = new SoulboundHandler(cred, issuer, actors);
105
+
106
+ targetContract(address(handler));
107
+
108
+ bytes4[] memory selectors = new bytes4[](3);
109
+ selectors[0] = handler.issue.selector;
110
+ selectors[1] = handler.revoke.selector;
111
+ selectors[2] = handler.tryTransfer.selector;
112
+ targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
113
+ }
114
+
115
+ function invariant_SumOfBalancesEqualsTotalIssued() public view {
116
+ uint256 sum;
117
+ for (uint256 i = 0; i < actors.length; i++) {
118
+ sum += cred.balanceOf(actors[i]);
119
+ }
120
+ assertEq(sum, cred.totalIssued(), "sum(balances) != totalIssued (transfer/leak detected)");
121
+ }
122
+
123
+ function invariant_RevokedAreNeverValid() public view {
124
+ uint256 n = handler.revokedCount();
125
+ for (uint256 i = 0; i < n; i++) {
126
+ assertFalse(cred.isValid(handler.revokedIdAt(i)), "revoked credential reported valid");
127
+ }
128
+ }
129
+
130
+ function invariant_TotalIssuedMatchesSuccessfulIssues() public view {
131
+ assertEq(cred.totalIssued(), handler.successfulIssues(), "totalIssued != successful issues");
132
+ }
133
+ }