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,281 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.34;
|
|
3
|
+
|
|
4
|
+
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
|
5
|
+
import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol";
|
|
6
|
+
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
|
|
7
|
+
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
|
|
8
|
+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @title KozaCollection
|
|
12
|
+
* @author kozalak-L1 contributors
|
|
13
|
+
* @notice ERC-721 + ERC-2981 royalty + Ownable2Step + Merkle allowlist NFT koleksiyonu.
|
|
14
|
+
* Avalanche L1 veya C-Chain üzerinde audit-grade boilerplate.
|
|
15
|
+
* @dev OpenZeppelin v5.3+ pattern'leri. Token id'ler 1'den başlar (UX/explorer kolaylığı).
|
|
16
|
+
*
|
|
17
|
+
* Faz akışı:
|
|
18
|
+
* Closed → mint kapalı, sadece deploy sonrası başlangıç
|
|
19
|
+
* Allowlist → sadece Merkle proof'u doğrulanan adresler (tek seferlik claim)
|
|
20
|
+
* Public → herkese açık
|
|
21
|
+
*
|
|
22
|
+
* Güvenlik kararları:
|
|
23
|
+
* - Ownable2Step: yanlış adrese ownership transferi engellenir
|
|
24
|
+
* - immutable maxSupply: deploy sonrası kapasite değiştirilemez
|
|
25
|
+
* - Strict msg.value: fazla ödeme refund edilmez (basit, deterministik). Frontend
|
|
26
|
+
* kullanıcıya net miktarı göstermek zorunda
|
|
27
|
+
* - Merkle leaf = keccak256(abi.encodePacked(address)) — basit, sadece "kim"i
|
|
28
|
+
* kanıtlar; quota MAX_PER_WALLET ile sınırlandırılır
|
|
29
|
+
* - Per-wallet mint cap (her fazı kapsar) Sybil saldırılarını yavaşlatır ama
|
|
30
|
+
* çoklu cüzdan tamamen engellenmez (KYC seviyesinde değil)
|
|
31
|
+
* - withdraw `call`'i target'a istediği gibi davranma yetkisi verir; reentrancy
|
|
32
|
+
* riski yok çünkü mint/setter fonksiyonlarda durum güncellemeleri tüm external
|
|
33
|
+
* çağrılardan ÖNCE yapılır (CEI pattern)
|
|
34
|
+
* - ERC-2981 royalty marketplace'in saygı göstermesine bağlıdır (OpenSea Operator
|
|
35
|
+
* Filter, Joepegs vb. — protokol seviyesinde garantisi yoktur)
|
|
36
|
+
*/
|
|
37
|
+
contract KozaCollection is ERC721, ERC2981, Ownable2Step {
|
|
38
|
+
using Strings for uint256;
|
|
39
|
+
|
|
40
|
+
/*//////////////////////////////////////////////////////////////
|
|
41
|
+
TYPES
|
|
42
|
+
//////////////////////////////////////////////////////////////*/
|
|
43
|
+
|
|
44
|
+
enum Phase {
|
|
45
|
+
Closed,
|
|
46
|
+
Allowlist,
|
|
47
|
+
Public
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*//////////////////////////////////////////////////////////////
|
|
51
|
+
CONSTANTS
|
|
52
|
+
//////////////////////////////////////////////////////////////*/
|
|
53
|
+
|
|
54
|
+
/// @notice Tek bir cüzdanın tüm fazlar boyunca toplam mint edebileceği üst sınır.
|
|
55
|
+
uint256 public constant MAX_PER_WALLET = 10;
|
|
56
|
+
|
|
57
|
+
/*//////////////////////////////////////////////////////////////
|
|
58
|
+
STORAGE
|
|
59
|
+
//////////////////////////////////////////////////////////////*/
|
|
60
|
+
|
|
61
|
+
/// @notice Toplam basılabilir token sayısı (deploy'da sabit, immutable).
|
|
62
|
+
uint256 public immutable maxSupply;
|
|
63
|
+
|
|
64
|
+
/// @notice Şimdiye kadar basılan toplam token sayısı (id artışı için kullanılır,
|
|
65
|
+
/// burn olsa bile geri sayılmaz).
|
|
66
|
+
uint256 public totalMinted;
|
|
67
|
+
|
|
68
|
+
/// @notice Mint başına ödenecek native gas miktarı (wei).
|
|
69
|
+
uint256 public mintPrice;
|
|
70
|
+
|
|
71
|
+
/// @notice Allowlist Merkle ağacının kök hash'i.
|
|
72
|
+
bytes32 public merkleRoot;
|
|
73
|
+
|
|
74
|
+
/// @notice Aktif mint fazı.
|
|
75
|
+
Phase public phase;
|
|
76
|
+
|
|
77
|
+
/// @dev `tokenURI` için `_baseURI()` override sonucu.
|
|
78
|
+
string private _baseTokenURI;
|
|
79
|
+
|
|
80
|
+
/// @notice Cüzdan başına şimdiye kadarki mint sayısı (allowlist + public toplam).
|
|
81
|
+
mapping(address account => uint256 minted) public mintedPerWallet;
|
|
82
|
+
|
|
83
|
+
/// @notice Allowlist'i bir kez kullanan adresler (yeniden claim engeli).
|
|
84
|
+
mapping(address account => bool claimed) public allowlistClaimed;
|
|
85
|
+
|
|
86
|
+
/*//////////////////////////////////////////////////////////////
|
|
87
|
+
EVENTS
|
|
88
|
+
//////////////////////////////////////////////////////////////*/
|
|
89
|
+
|
|
90
|
+
event PhaseChanged(Phase indexed newPhase);
|
|
91
|
+
event MerkleRootSet(bytes32 indexed newRoot);
|
|
92
|
+
event BaseURISet(string newBaseURI);
|
|
93
|
+
event MintPriceSet(uint256 newPrice);
|
|
94
|
+
event Withdrawn(address indexed to, uint256 amount);
|
|
95
|
+
|
|
96
|
+
/*//////////////////////////////////////////////////////////////
|
|
97
|
+
ERRORS
|
|
98
|
+
//////////////////////////////////////////////////////////////*/
|
|
99
|
+
|
|
100
|
+
error MaxSupplyZero();
|
|
101
|
+
error MaxSupplyReached(uint256 supplyCap, uint256 attempted);
|
|
102
|
+
error InvalidProof();
|
|
103
|
+
error AlreadyClaimed(address account);
|
|
104
|
+
error IncorrectPayment(uint256 sent, uint256 required);
|
|
105
|
+
error WithdrawFailed();
|
|
106
|
+
error WrongPhase(Phase current, Phase required);
|
|
107
|
+
error ZeroQuantity();
|
|
108
|
+
error ExceedsPerWalletLimit(uint256 current, uint256 attempted, uint256 max);
|
|
109
|
+
|
|
110
|
+
/*//////////////////////////////////////////////////////////////
|
|
111
|
+
CONSTRUCTOR
|
|
112
|
+
//////////////////////////////////////////////////////////////*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param name_ Koleksiyon adı (örn. "Koza Genesis")
|
|
116
|
+
* @param symbol_ Koleksiyon sembolü (örn. "KOZA")
|
|
117
|
+
* @param baseURI_ Metadata IPFS/HTTPS prefix (sonu `/` ile bitmeli, tokenURI = baseURI + id)
|
|
118
|
+
* @param maxSupply_ Toplam basılabilir token sayısı (immutable)
|
|
119
|
+
* @param mintPrice_ Token başına native gas ücreti (wei, 0 olabilir)
|
|
120
|
+
* @param royaltyReceiver_ Default ERC-2981 royalty alıcısı (multisig önerilir)
|
|
121
|
+
* @param royaltyBps_ Royalty oranı BPS cinsinden (1000 = %10, max 10000 = %100)
|
|
122
|
+
* @param initialOwner_ Owner adresi (production: multisig, asla EOA değil)
|
|
123
|
+
*/
|
|
124
|
+
constructor(
|
|
125
|
+
string memory name_,
|
|
126
|
+
string memory symbol_,
|
|
127
|
+
string memory baseURI_,
|
|
128
|
+
uint256 maxSupply_,
|
|
129
|
+
uint256 mintPrice_,
|
|
130
|
+
address royaltyReceiver_,
|
|
131
|
+
uint96 royaltyBps_,
|
|
132
|
+
address initialOwner_
|
|
133
|
+
)
|
|
134
|
+
ERC721(name_, symbol_)
|
|
135
|
+
Ownable(initialOwner_)
|
|
136
|
+
{
|
|
137
|
+
if (maxSupply_ == 0) revert MaxSupplyZero();
|
|
138
|
+
|
|
139
|
+
maxSupply = maxSupply_;
|
|
140
|
+
mintPrice = mintPrice_;
|
|
141
|
+
_baseTokenURI = baseURI_;
|
|
142
|
+
|
|
143
|
+
// OZ ERC2981 royaltyBps_ <= _feeDenominator() (10000) kontrolünü kendi içinde yapar
|
|
144
|
+
// ve receiver_ != address(0) kontrolü uygular. Ek revert tanımına gerek yok.
|
|
145
|
+
_setDefaultRoyalty(royaltyReceiver_, royaltyBps_);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/*//////////////////////////////////////////////////////////////
|
|
149
|
+
MINTING
|
|
150
|
+
//////////////////////////////////////////////////////////////*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @notice Allowlist fazında, Merkle proof ile mint et.
|
|
154
|
+
* @dev Tek seferlik claim. Aynı adres ikinci kez allowlistMint çağıramaz; ama public
|
|
155
|
+
* faza geçtiğinde publicMint yapabilir (MAX_PER_WALLET izin verdiği kadar).
|
|
156
|
+
* @param quantity Bu çağrıda mint edilecek miktar
|
|
157
|
+
* @param proof Çağıranın Merkle ağacındaki yaprak kanıtı
|
|
158
|
+
*/
|
|
159
|
+
function allowlistMint(uint256 quantity, bytes32[] calldata proof) external payable {
|
|
160
|
+
if (phase != Phase.Allowlist) revert WrongPhase(phase, Phase.Allowlist);
|
|
161
|
+
if (allowlistClaimed[msg.sender]) revert AlreadyClaimed(msg.sender);
|
|
162
|
+
|
|
163
|
+
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
|
|
164
|
+
if (!MerkleProof.verifyCalldata(proof, merkleRoot, leaf)) revert InvalidProof();
|
|
165
|
+
|
|
166
|
+
// CEI: önce state, sonra ödeme + mint
|
|
167
|
+
allowlistClaimed[msg.sender] = true;
|
|
168
|
+
_mintBatch(msg.sender, quantity);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @notice Public faz mint. Herkese açık.
|
|
173
|
+
* @param quantity Bu çağrıda mint edilecek miktar
|
|
174
|
+
*/
|
|
175
|
+
function publicMint(uint256 quantity) external payable {
|
|
176
|
+
if (phase != Phase.Public) revert WrongPhase(phase, Phase.Public);
|
|
177
|
+
_mintBatch(msg.sender, quantity);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/*//////////////////////////////////////////////////////////////
|
|
181
|
+
OWNER ACTIONS
|
|
182
|
+
//////////////////////////////////////////////////////////////*/
|
|
183
|
+
|
|
184
|
+
/// @notice Allowlist Merkle ağacının yeni kökünü ata.
|
|
185
|
+
function setMerkleRoot(bytes32 newRoot) external onlyOwner {
|
|
186
|
+
merkleRoot = newRoot;
|
|
187
|
+
emit MerkleRootSet(newRoot);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// @notice Aktif mint fazını değiştir.
|
|
191
|
+
function setPhase(Phase newPhase) external onlyOwner {
|
|
192
|
+
phase = newPhase;
|
|
193
|
+
emit PhaseChanged(newPhase);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// @notice tokenURI prefix'ini güncelle (örn. reveal sonrası).
|
|
197
|
+
function setBaseURI(string calldata newBaseURI) external onlyOwner {
|
|
198
|
+
_baseTokenURI = newBaseURI;
|
|
199
|
+
emit BaseURISet(newBaseURI);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// @notice Token başına mint ücretini güncelle.
|
|
203
|
+
function setMintPrice(uint256 newPrice) external onlyOwner {
|
|
204
|
+
mintPrice = newPrice;
|
|
205
|
+
emit MintPriceSet(newPrice);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// @notice Default ERC-2981 royalty bilgisini güncelle.
|
|
209
|
+
/// @dev Tüm token'lar için varsayılan; per-token override yok (bu template'te).
|
|
210
|
+
function setDefaultRoyalty(address receiver, uint96 bps) external onlyOwner {
|
|
211
|
+
_setDefaultRoyalty(receiver, bps);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @notice Kontratta biriken native gas'ı hedef adrese çek.
|
|
216
|
+
* @dev `to` çağrı sırasında reentrancy yapamaz çünkü withdraw'da güncellenecek
|
|
217
|
+
* durum yok (balance native zaten transfer edilirken sıfırlanır). Yine de
|
|
218
|
+
* Withdrawn event'i call'dan önce emit edilir; başarısızsa revert tüm
|
|
219
|
+
* durumu geri alır.
|
|
220
|
+
*/
|
|
221
|
+
function withdraw(address payable to) external onlyOwner {
|
|
222
|
+
uint256 balance = address(this).balance;
|
|
223
|
+
emit Withdrawn(to, balance);
|
|
224
|
+
(bool success,) = to.call{value: balance}("");
|
|
225
|
+
if (!success) revert WithdrawFailed();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/*//////////////////////////////////////////////////////////////
|
|
229
|
+
VIEWS
|
|
230
|
+
//////////////////////////////////////////////////////////////*/
|
|
231
|
+
|
|
232
|
+
/// @inheritdoc ERC721
|
|
233
|
+
function _baseURI() internal view override returns (string memory) {
|
|
234
|
+
return _baseTokenURI;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// @notice Birden fazla interface (ERC721, ERC2981) desteklendiği için override.
|
|
238
|
+
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC2981) returns (bool) {
|
|
239
|
+
return super.supportsInterface(interfaceId);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/*//////////////////////////////////////////////////////////////
|
|
243
|
+
INTERNAL
|
|
244
|
+
//////////////////////////////////////////////////////////////*/
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @dev Mint helper'ı. Quantity, supply, per-wallet ve ödeme kontrollerini yapar,
|
|
248
|
+
* `_safeMint` ile id'leri sırayla basar.
|
|
249
|
+
*
|
|
250
|
+
* Strict `msg.value == required` kullanıyoruz — fazla ödeme refund edilmez.
|
|
251
|
+
* Bu basitlik audit yüzeyini küçültür ve frontend'in net hesap göstermesini
|
|
252
|
+
* zorunlu kılar. İhtiyaç durumunda fork'ta `>=` + refund eklenebilir.
|
|
253
|
+
*/
|
|
254
|
+
function _mintBatch(address to, uint256 quantity) private {
|
|
255
|
+
if (quantity == 0) revert ZeroQuantity();
|
|
256
|
+
|
|
257
|
+
uint256 currentMinted = mintedPerWallet[to];
|
|
258
|
+
if (currentMinted + quantity > MAX_PER_WALLET) {
|
|
259
|
+
revert ExceedsPerWalletLimit(currentMinted, quantity, MAX_PER_WALLET);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
uint256 totalAfter = totalMinted + quantity;
|
|
263
|
+
if (totalAfter > maxSupply) revert MaxSupplyReached(maxSupply, totalAfter);
|
|
264
|
+
|
|
265
|
+
uint256 required = mintPrice * quantity;
|
|
266
|
+
if (msg.value != required) revert IncorrectPayment(msg.value, required);
|
|
267
|
+
|
|
268
|
+
// CEI: state güncellemeleri _safeMint'ten ÖNCE yapılır (reentrancy guard)
|
|
269
|
+
mintedPerWallet[to] = currentMinted + quantity;
|
|
270
|
+
uint256 startId = totalMinted;
|
|
271
|
+
totalMinted = totalAfter;
|
|
272
|
+
|
|
273
|
+
for (uint256 i = 0; i < quantity;) {
|
|
274
|
+
// Token id 1'den başlasın (UX için)
|
|
275
|
+
_safeMint(to, startId + i + 1);
|
|
276
|
+
unchecked {
|
|
277
|
+
++i;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.34;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {DeployERC721Collection} from "../script/DeployERC721Collection.s.sol";
|
|
6
|
+
import {KozaCollection} from "../src/KozaCollection.sol";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @title DeployERC721CollectionTest
|
|
10
|
+
* @notice Smoke tests for the ERC-721 deploy script. Calls `deploy(...)` directly
|
|
11
|
+
* with explicit parameters; env-driven `run()` exercised on Fuji integration.
|
|
12
|
+
*/
|
|
13
|
+
contract DeployERC721CollectionTest is Test {
|
|
14
|
+
function test_Deploy_WithDefaultParameters() public {
|
|
15
|
+
DeployERC721Collection deployer = new DeployERC721Collection();
|
|
16
|
+
address broadcaster = makeAddr("broadcaster");
|
|
17
|
+
address royalty = makeAddr("royalty");
|
|
18
|
+
|
|
19
|
+
(KozaCollection nft, address returnedDeployer) = deployer.deploy(
|
|
20
|
+
"Koza Genesis", "KOZA", "ipfs://QmExampleCID/", 5000, 0.05 ether, royalty, 500, broadcaster, broadcaster
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
assertEq(nft.name(), "Koza Genesis");
|
|
24
|
+
assertEq(nft.symbol(), "KOZA");
|
|
25
|
+
assertEq(nft.maxSupply(), 5000);
|
|
26
|
+
assertEq(nft.mintPrice(), 0.05 ether);
|
|
27
|
+
assertEq(nft.owner(), broadcaster);
|
|
28
|
+
assertEq(uint8(nft.phase()), uint8(KozaCollection.Phase.Closed));
|
|
29
|
+
assertEq(returnedDeployer, broadcaster);
|
|
30
|
+
|
|
31
|
+
(address rcv, uint256 amt) = nft.royaltyInfo(1, 10_000);
|
|
32
|
+
assertEq(rcv, royalty);
|
|
33
|
+
assertEq(amt, 500);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function test_Deploy_WithCustomParameters() public {
|
|
37
|
+
DeployERC721Collection deployer = new DeployERC721Collection();
|
|
38
|
+
address customOwner = makeAddr("customOwner");
|
|
39
|
+
address customRoyalty = makeAddr("customRoyalty");
|
|
40
|
+
address customBroadcaster = makeAddr("customBroadcaster");
|
|
41
|
+
|
|
42
|
+
(KozaCollection nft,) = deployer.deploy(
|
|
43
|
+
"Custom Art",
|
|
44
|
+
"ART",
|
|
45
|
+
"https://meta.example/",
|
|
46
|
+
100,
|
|
47
|
+
0.001 ether,
|
|
48
|
+
customRoyalty,
|
|
49
|
+
1000,
|
|
50
|
+
customOwner,
|
|
51
|
+
customBroadcaster
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
assertEq(nft.name(), "Custom Art");
|
|
55
|
+
assertEq(nft.maxSupply(), 100);
|
|
56
|
+
assertEq(nft.owner(), customOwner);
|
|
57
|
+
|
|
58
|
+
(address rcv, uint256 amt) = nft.royaltyInfo(1, 10_000);
|
|
59
|
+
assertEq(rcv, customRoyalty);
|
|
60
|
+
assertEq(amt, 1000); // %10
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function test_Deploy_FreeMintNoRoyalty() public {
|
|
64
|
+
DeployERC721Collection deployer = new DeployERC721Collection();
|
|
65
|
+
address broadcaster = makeAddr("broadcaster");
|
|
66
|
+
address royalty = makeAddr("royalty");
|
|
67
|
+
|
|
68
|
+
(KozaCollection nft,) =
|
|
69
|
+
deployer.deploy("Free Drop", "FREE", "ipfs://QmFreeCID/", 1000, 0, royalty, 0, broadcaster, broadcaster);
|
|
70
|
+
|
|
71
|
+
assertEq(nft.mintPrice(), 0);
|
|
72
|
+
|
|
73
|
+
(, uint256 amt) = nft.royaltyInfo(1, 10_000);
|
|
74
|
+
assertEq(amt, 0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.34;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {KozaCollection} from "../src/KozaCollection.sol";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @title ERC721CollectionHandler
|
|
9
|
+
* @notice Foundry invariant testing handler. Bounded random calls into KozaCollection.
|
|
10
|
+
* @dev Mints and transfers between actors. Phase ve fiyat değişiklikleri owner tarafından
|
|
11
|
+
* yapılır. Invariant runner her tx sonrası invariant fonksiyonlarını çağırır.
|
|
12
|
+
*/
|
|
13
|
+
contract ERC721CollectionHandler is Test {
|
|
14
|
+
KozaCollection public nft;
|
|
15
|
+
address public owner;
|
|
16
|
+
address[] public actors;
|
|
17
|
+
|
|
18
|
+
uint256 public callCount;
|
|
19
|
+
|
|
20
|
+
/// @notice Toplam kontrata ödenen ETH (mint ile). Withdraw'lar ayrı tutulur.
|
|
21
|
+
uint256 public totalPaid;
|
|
22
|
+
|
|
23
|
+
/// @notice Owner tarafından çekilen toplam ETH (cumulative).
|
|
24
|
+
uint256 public totalWithdrawn;
|
|
25
|
+
|
|
26
|
+
constructor(KozaCollection _nft, address _owner, address[] memory _actors) {
|
|
27
|
+
nft = _nft;
|
|
28
|
+
owner = _owner;
|
|
29
|
+
actors = _actors;
|
|
30
|
+
|
|
31
|
+
// Faz başlangıçta Public — handler basit kalsın diye
|
|
32
|
+
vm.prank(owner);
|
|
33
|
+
nft.setPhase(KozaCollection.Phase.Public);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _pickActor(uint256 seed) internal view returns (address) {
|
|
37
|
+
return actors[seed % actors.length];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// @notice Random public mint by an actor.
|
|
41
|
+
function publicMint(uint256 actorSeed, uint256 quantity) external {
|
|
42
|
+
callCount++;
|
|
43
|
+
address actor = _pickActor(actorSeed);
|
|
44
|
+
|
|
45
|
+
uint256 maxSupply = nft.maxSupply();
|
|
46
|
+
uint256 minted = nft.totalMinted();
|
|
47
|
+
if (minted >= maxSupply) return;
|
|
48
|
+
|
|
49
|
+
uint256 walletMinted = nft.mintedPerWallet(actor);
|
|
50
|
+
uint256 maxPerWallet = nft.MAX_PER_WALLET();
|
|
51
|
+
if (walletMinted >= maxPerWallet) return;
|
|
52
|
+
|
|
53
|
+
uint256 maxQty = maxSupply - minted;
|
|
54
|
+
uint256 walletRoom = maxPerWallet - walletMinted;
|
|
55
|
+
uint256 cap = maxQty < walletRoom ? maxQty : walletRoom;
|
|
56
|
+
|
|
57
|
+
quantity = bound(quantity, 1, cap);
|
|
58
|
+
uint256 cost = nft.mintPrice() * quantity;
|
|
59
|
+
|
|
60
|
+
vm.deal(actor, cost);
|
|
61
|
+
|
|
62
|
+
vm.prank(actor);
|
|
63
|
+
try nft.publicMint{value: cost}(quantity) {
|
|
64
|
+
totalPaid += cost;
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// @notice Random NFT transfer between actors.
|
|
69
|
+
function transfer(uint256 fromSeed, uint256 toSeed, uint256 idSeed) external {
|
|
70
|
+
callCount++;
|
|
71
|
+
address from = _pickActor(fromSeed);
|
|
72
|
+
address to = _pickActor(toSeed);
|
|
73
|
+
|
|
74
|
+
uint256 balance = nft.balanceOf(from);
|
|
75
|
+
if (balance == 0) return;
|
|
76
|
+
|
|
77
|
+
// Token id'leri 1..totalMinted aralığında. From'un sahip olduğu bir id'yi bul.
|
|
78
|
+
uint256 totalMinted = nft.totalMinted();
|
|
79
|
+
if (totalMinted == 0) return;
|
|
80
|
+
|
|
81
|
+
uint256 startId = (idSeed % totalMinted) + 1;
|
|
82
|
+
uint256 foundId;
|
|
83
|
+
for (uint256 i = 0; i < totalMinted; i++) {
|
|
84
|
+
uint256 candidate = ((startId - 1 + i) % totalMinted) + 1;
|
|
85
|
+
if (nft.ownerOf(candidate) == from) {
|
|
86
|
+
foundId = candidate;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (foundId == 0) return;
|
|
91
|
+
|
|
92
|
+
vm.prank(from);
|
|
93
|
+
try nft.transferFrom(from, to, foundId) {} catch {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// @notice Owner withdraw kontratı boşaltır.
|
|
97
|
+
function withdraw() external {
|
|
98
|
+
callCount++;
|
|
99
|
+
uint256 balance = address(nft).balance;
|
|
100
|
+
if (balance == 0) return;
|
|
101
|
+
|
|
102
|
+
vm.prank(owner);
|
|
103
|
+
try nft.withdraw(payable(owner)) {
|
|
104
|
+
totalWithdrawn += balance;
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @title ERC721CollectionInvariantTest
|
|
111
|
+
* @notice Stateful fuzzing invariants for KozaCollection.
|
|
112
|
+
*
|
|
113
|
+
* Tested invariants:
|
|
114
|
+
* 1. totalMinted <= maxSupply
|
|
115
|
+
* 2. sum(balanceOf(actors)) == totalMinted (no token leaks)
|
|
116
|
+
* 3. contract.balance == totalPaid - totalWithdrawn
|
|
117
|
+
* 4. owner immutable during fuzz
|
|
118
|
+
*/
|
|
119
|
+
contract ERC721CollectionInvariantTest is Test {
|
|
120
|
+
KozaCollection internal nft;
|
|
121
|
+
ERC721CollectionHandler internal handler;
|
|
122
|
+
address internal owner;
|
|
123
|
+
address[] internal actors;
|
|
124
|
+
|
|
125
|
+
string internal constant NAME = "Koza Genesis";
|
|
126
|
+
string internal constant SYMBOL = "KOZA";
|
|
127
|
+
string internal constant BASE_URI = "ipfs://QmExampleCID/";
|
|
128
|
+
uint256 internal constant MAX_SUPPLY = 50;
|
|
129
|
+
uint256 internal constant MINT_PRICE = 0.01 ether;
|
|
130
|
+
uint96 internal constant ROYALTY_BPS = 500;
|
|
131
|
+
|
|
132
|
+
function setUp() public {
|
|
133
|
+
owner = makeAddr("owner");
|
|
134
|
+
|
|
135
|
+
actors.push(makeAddr("alice"));
|
|
136
|
+
actors.push(makeAddr("bob"));
|
|
137
|
+
actors.push(makeAddr("charlie"));
|
|
138
|
+
actors.push(makeAddr("dave"));
|
|
139
|
+
actors.push(makeAddr("eve"));
|
|
140
|
+
|
|
141
|
+
address royalty = makeAddr("royalty");
|
|
142
|
+
nft = new KozaCollection(NAME, SYMBOL, BASE_URI, MAX_SUPPLY, MINT_PRICE, royalty, ROYALTY_BPS, owner);
|
|
143
|
+
handler = new ERC721CollectionHandler(nft, owner, actors);
|
|
144
|
+
|
|
145
|
+
targetContract(address(handler));
|
|
146
|
+
|
|
147
|
+
bytes4[] memory selectors = new bytes4[](3);
|
|
148
|
+
selectors[0] = handler.publicMint.selector;
|
|
149
|
+
selectors[1] = handler.transfer.selector;
|
|
150
|
+
selectors[2] = handler.withdraw.selector;
|
|
151
|
+
targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function invariant_TotalMintedDoesNotExceedMaxSupply() public view {
|
|
155
|
+
assertLe(nft.totalMinted(), nft.maxSupply(), "totalMinted > maxSupply");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function invariant_SumOfBalancesEqualsTotalMinted() public view {
|
|
159
|
+
uint256 sum;
|
|
160
|
+
for (uint256 i = 0; i < actors.length; i++) {
|
|
161
|
+
sum += nft.balanceOf(actors[i]);
|
|
162
|
+
}
|
|
163
|
+
assertEq(sum, nft.totalMinted(), "sum(balances) != totalMinted (token leak)");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function invariant_ContractBalanceMatchesPaidMinusWithdrawn() public view {
|
|
167
|
+
assertEq(
|
|
168
|
+
address(nft).balance, handler.totalPaid() - handler.totalWithdrawn(), "contract balance != paid - withdrawn"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function invariant_OwnerDoesNotChange() public view {
|
|
173
|
+
assertEq(nft.owner(), owner, "owner mutated unexpectedly");
|
|
174
|
+
}
|
|
175
|
+
}
|