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