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,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,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,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
|
+
}
|