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,131 @@
|
|
|
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 {KozaGasToken} from "../src/KozaGasToken.sol";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @title DeployERC20Gas
|
|
10
|
+
* @notice Foundry deployment script for KozaGasToken (Phase 1, Template 1).
|
|
11
|
+
*
|
|
12
|
+
* Usage (Fuji testnet, with .env populated):
|
|
13
|
+
*
|
|
14
|
+
* forge script script/deploy/DeployERC20Gas.s.sol \
|
|
15
|
+
* --rpc-url fuji \
|
|
16
|
+
* --broadcast \
|
|
17
|
+
* --verify
|
|
18
|
+
*
|
|
19
|
+
* Required env (loaded by `forge` from .env):
|
|
20
|
+
* - PRIVATE_KEY — deployer private key (testnet only!)
|
|
21
|
+
*
|
|
22
|
+
* Optional env (defaults below if unset):
|
|
23
|
+
* - ERC20_NAME ("Koza Gas Token")
|
|
24
|
+
* - ERC20_SYMBOL ("KGAS")
|
|
25
|
+
* - ERC20_CAP (1_000_000 ether) — total supply hard cap
|
|
26
|
+
* - ERC20_INITIAL_MINT (100_000 ether) — minted to owner at deploy
|
|
27
|
+
* - ERC20_OWNER (msg.sender) — production: pass a multisig
|
|
28
|
+
*
|
|
29
|
+
* Production checklist:
|
|
30
|
+
* - PRIVATE_KEY must be testnet-only OR replaced with cast-wallet/keystore
|
|
31
|
+
* - ERC20_OWNER must point to a Safe (Gnosis Safe) multisig, never an EOA
|
|
32
|
+
* - SNOWTRACE_API_KEY in .env (set to "verifyContract" for free Routescan tier)
|
|
33
|
+
*/
|
|
34
|
+
contract DeployERC20Gas is Script {
|
|
35
|
+
/*//////////////////////////////////////////////////////////////
|
|
36
|
+
DEFAULTS
|
|
37
|
+
//////////////////////////////////////////////////////////////*/
|
|
38
|
+
|
|
39
|
+
string internal constant DEFAULT_NAME = "Koza Gas Token";
|
|
40
|
+
string internal constant DEFAULT_SYMBOL = "KGAS";
|
|
41
|
+
uint256 internal constant DEFAULT_CAP = 1_000_000 ether;
|
|
42
|
+
uint256 internal constant DEFAULT_INITIAL_MINT = 100_000 ether;
|
|
43
|
+
|
|
44
|
+
/*//////////////////////////////////////////////////////////////
|
|
45
|
+
RUN
|
|
46
|
+
//////////////////////////////////////////////////////////////*/
|
|
47
|
+
|
|
48
|
+
/// @notice Entry point invoked by `forge script`. Reads parameters from env.
|
|
49
|
+
function run() external returns (KozaGasToken token, address deployer) {
|
|
50
|
+
string memory name = vm.envOr("ERC20_NAME", DEFAULT_NAME);
|
|
51
|
+
string memory symbol = vm.envOr("ERC20_SYMBOL", DEFAULT_SYMBOL);
|
|
52
|
+
uint256 cap = vm.envOr("ERC20_CAP", DEFAULT_CAP);
|
|
53
|
+
uint256 initialMint = vm.envOr("ERC20_INITIAL_MINT", DEFAULT_INITIAL_MINT);
|
|
54
|
+
|
|
55
|
+
address broadcaster = _resolveBroadcaster();
|
|
56
|
+
address owner = vm.envOr("ERC20_OWNER", broadcaster);
|
|
57
|
+
|
|
58
|
+
return deploy(name, symbol, cap, initialMint, owner, broadcaster);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// @notice Test-friendly entry point. Takes explicit parameters instead of env.
|
|
62
|
+
/// @dev Prefer this from Foundry tests so env state does not leak between cases.
|
|
63
|
+
function deploy(
|
|
64
|
+
string memory name,
|
|
65
|
+
string memory symbol,
|
|
66
|
+
uint256 cap,
|
|
67
|
+
uint256 initialMint,
|
|
68
|
+
address owner,
|
|
69
|
+
address broadcaster
|
|
70
|
+
)
|
|
71
|
+
public
|
|
72
|
+
returns (KozaGasToken token, address deployer)
|
|
73
|
+
{
|
|
74
|
+
_logPreDeploy(name, symbol, cap, initialMint, owner, broadcaster);
|
|
75
|
+
|
|
76
|
+
vm.startBroadcast();
|
|
77
|
+
token = new KozaGasToken(name, symbol, cap, initialMint, owner);
|
|
78
|
+
vm.stopBroadcast();
|
|
79
|
+
|
|
80
|
+
deployer = broadcaster;
|
|
81
|
+
|
|
82
|
+
_logPostDeploy(token, owner);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/*//////////////////////////////////////////////////////////////
|
|
86
|
+
INTERNALS
|
|
87
|
+
//////////////////////////////////////////////////////////////*/
|
|
88
|
+
|
|
89
|
+
/// @dev Returns the address that will broadcast txs for this run.
|
|
90
|
+
/// Order: explicit DEPLOYER_ADDRESS > address derived from PRIVATE_KEY > tx.origin.
|
|
91
|
+
function _resolveBroadcaster() internal view returns (address) {
|
|
92
|
+
address explicitDeployer = vm.envOr("DEPLOYER_ADDRESS", address(0));
|
|
93
|
+
if (explicitDeployer != address(0)) return explicitDeployer;
|
|
94
|
+
|
|
95
|
+
uint256 pk = vm.envOr("PRIVATE_KEY", uint256(0));
|
|
96
|
+
if (pk != 0) return vm.addr(pk);
|
|
97
|
+
|
|
98
|
+
return tx.origin;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _logPreDeploy(
|
|
102
|
+
string memory name,
|
|
103
|
+
string memory symbol,
|
|
104
|
+
uint256 cap,
|
|
105
|
+
uint256 initialMint,
|
|
106
|
+
address owner,
|
|
107
|
+
address broadcaster
|
|
108
|
+
)
|
|
109
|
+
internal
|
|
110
|
+
pure
|
|
111
|
+
{
|
|
112
|
+
console2.log("=== Deploying KozaGasToken ===");
|
|
113
|
+
console2.log(" Broadcaster: ", broadcaster);
|
|
114
|
+
console2.log(" Owner: ", owner);
|
|
115
|
+
console2.log(" Name: ", name);
|
|
116
|
+
console2.log(" Symbol: ", symbol);
|
|
117
|
+
console2.log(" Cap (wei): ", cap);
|
|
118
|
+
console2.log(" Initial mint: ", initialMint);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _logPostDeploy(KozaGasToken token, address owner) internal view {
|
|
122
|
+
console2.log("=== Deployed ===");
|
|
123
|
+
console2.log(" Address: ", address(token));
|
|
124
|
+
console2.log(" Total supply: ", token.totalSupply());
|
|
125
|
+
console2.log(" Cap: ", token.cap());
|
|
126
|
+
console2.log(" Owner balance: ", token.balanceOf(owner));
|
|
127
|
+
console2.log("");
|
|
128
|
+
console2.log("Verify on Snowtrace:");
|
|
129
|
+
console2.log(" forge verify-contract <address> KozaGasToken --rpc-url fuji");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.34;
|
|
3
|
+
|
|
4
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
5
|
+
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
|
|
6
|
+
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
|
|
7
|
+
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @title KozaGasToken
|
|
11
|
+
* @author kozalak-L1 contributors
|
|
12
|
+
* @notice ERC-20 + Capped + Permit + Ownable2Step token. Subnet-EVM L1'de native gas
|
|
13
|
+
* token olarak veya C-Chain üzerinde yardımcı token olarak kullanılabilir.
|
|
14
|
+
* @dev OpenZeppelin v5.3+ pattern'leri ile audit-grade boilerplate. Custom logic minimum.
|
|
15
|
+
*
|
|
16
|
+
* Güvenlik:
|
|
17
|
+
* - Ownable2Step: tek-adımlı ownership transfer riski sıfırlanır (yanlış adres koruması)
|
|
18
|
+
* - ERC20Capped: total supply hard-cap, sınırsız enflasyon yok
|
|
19
|
+
* - ERC20Permit (EIP-2612): gasless approve, smart wallet UX
|
|
20
|
+
* - Custom errors: gas + audit kalitesi (require string yerine)
|
|
21
|
+
*
|
|
22
|
+
* Constructor input validation OpenZeppelin parent kontratlarına bırakıldı:
|
|
23
|
+
* - cap_ == 0 → ERC20Capped.ERC20InvalidCap(0)
|
|
24
|
+
* - initialOwner_ 0 → Ownable.OwnableInvalidOwner(0)
|
|
25
|
+
* Bu sayede çift kontrol (gas waste) ve hata mesajı çakışması olmaz.
|
|
26
|
+
*/
|
|
27
|
+
contract KozaGasToken is ERC20Capped, ERC20Permit, Ownable2Step {
|
|
28
|
+
/*//////////////////////////////////////////////////////////////
|
|
29
|
+
ERRORS
|
|
30
|
+
//////////////////////////////////////////////////////////////*/
|
|
31
|
+
|
|
32
|
+
/// @notice Sıfır miktar parametresi geçerli değil (mint, burn için).
|
|
33
|
+
error ZeroAmount();
|
|
34
|
+
|
|
35
|
+
/// @notice Constructor'daki ilk mint cap'i aşıyor.
|
|
36
|
+
/// @param cap Maksimum total supply
|
|
37
|
+
/// @param attempted İstenen ilk mint miktarı
|
|
38
|
+
error InitialMintExceedsCap(uint256 cap, uint256 attempted);
|
|
39
|
+
|
|
40
|
+
/*//////////////////////////////////////////////////////////////
|
|
41
|
+
CONSTRUCTOR
|
|
42
|
+
//////////////////////////////////////////////////////////////*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param name_ Token adı (örn. "Koza Gas Token")
|
|
46
|
+
* @param symbol_ Token sembolü (örn. "KGAS")
|
|
47
|
+
* @param cap_ Maksimum total supply (wei cinsinden, decimal'i içerir)
|
|
48
|
+
* @param initialMint_ Constructor'da `initialOwner_`'a mint edilecek miktar (0 olabilir)
|
|
49
|
+
* @param initialOwner_ Owner adresi (production: multisig, asla EOA değil)
|
|
50
|
+
*/
|
|
51
|
+
constructor(
|
|
52
|
+
string memory name_,
|
|
53
|
+
string memory symbol_,
|
|
54
|
+
uint256 cap_,
|
|
55
|
+
uint256 initialMint_,
|
|
56
|
+
address initialOwner_
|
|
57
|
+
)
|
|
58
|
+
ERC20(name_, symbol_)
|
|
59
|
+
ERC20Capped(cap_)
|
|
60
|
+
ERC20Permit(name_)
|
|
61
|
+
Ownable(initialOwner_)
|
|
62
|
+
{
|
|
63
|
+
if (initialMint_ > cap_) {
|
|
64
|
+
revert InitialMintExceedsCap(cap_, initialMint_);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (initialMint_ > 0) {
|
|
68
|
+
_mint(initialOwner_, initialMint_);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*//////////////////////////////////////////////////////////////
|
|
73
|
+
EXTERNAL ACTIONS
|
|
74
|
+
//////////////////////////////////////////////////////////////*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @notice Yeni token mint et. Sadece owner çağırabilir.
|
|
78
|
+
* @dev Cap kontrolü ERC20Capped tarafından `_update` içinde otomatik yapılır.
|
|
79
|
+
* Sıfır adres kontrolü ERC20 `_mint` içinde yapılır (ERC20InvalidReceiver).
|
|
80
|
+
* @param to Mint edilecek adres
|
|
81
|
+
* @param amount Mint miktarı (wei cinsinden)
|
|
82
|
+
*/
|
|
83
|
+
function mint(address to, uint256 amount) external onlyOwner {
|
|
84
|
+
if (amount == 0) revert ZeroAmount();
|
|
85
|
+
_mint(to, amount);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @notice Çağıran kişi kendi token'larını yakar.
|
|
90
|
+
* @param amount Yakılacak miktar (wei cinsinden)
|
|
91
|
+
*/
|
|
92
|
+
function burn(uint256 amount) external {
|
|
93
|
+
if (amount == 0) revert ZeroAmount();
|
|
94
|
+
_burn(msg.sender, amount);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*//////////////////////////////////////////////////////////////
|
|
98
|
+
OVERRIDES
|
|
99
|
+
//////////////////////////////////////////////////////////////*/
|
|
100
|
+
|
|
101
|
+
/// @dev ERC20 ve ERC20Capped'in `_update` fonksiyonları çakışıyor; üst sınıfa delegate et.
|
|
102
|
+
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Capped) {
|
|
103
|
+
super._update(from, to, value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.34;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {DeployERC20Gas} from "../script/DeployERC20Gas.s.sol";
|
|
6
|
+
import {KozaGasToken} from "../src/KozaGasToken.sol";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @title DeployERC20GasTest
|
|
10
|
+
* @notice Smoke tests for the deploy script. Calls `deploy(...)` directly with
|
|
11
|
+
* explicit parameters so each test is order-independent and immune to
|
|
12
|
+
* leaking env state between Foundry cases.
|
|
13
|
+
*
|
|
14
|
+
* The env-driven `run()` entry point is intentionally not unit-tested here;
|
|
15
|
+
* it is exercised via the actual `forge script ... --broadcast` flow on
|
|
16
|
+
* Fuji testnet (integration test).
|
|
17
|
+
*/
|
|
18
|
+
contract DeployERC20GasTest is Test {
|
|
19
|
+
function test_Deploy_WithDefaultParameters() public {
|
|
20
|
+
DeployERC20Gas deployer = new DeployERC20Gas();
|
|
21
|
+
address broadcaster = makeAddr("broadcaster");
|
|
22
|
+
|
|
23
|
+
(KozaGasToken token, address returnedDeployer) =
|
|
24
|
+
deployer.deploy("Koza Gas Token", "KGAS", 1_000_000 ether, 100_000 ether, broadcaster, broadcaster);
|
|
25
|
+
|
|
26
|
+
assertEq(token.name(), "Koza Gas Token");
|
|
27
|
+
assertEq(token.symbol(), "KGAS");
|
|
28
|
+
assertEq(token.cap(), 1_000_000 ether);
|
|
29
|
+
assertEq(token.totalSupply(), 100_000 ether);
|
|
30
|
+
assertEq(token.owner(), broadcaster);
|
|
31
|
+
assertEq(token.balanceOf(broadcaster), 100_000 ether);
|
|
32
|
+
assertEq(returnedDeployer, broadcaster);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function test_Deploy_WithCustomParameters() public {
|
|
36
|
+
DeployERC20Gas deployer = new DeployERC20Gas();
|
|
37
|
+
address customOwner = makeAddr("customOwner");
|
|
38
|
+
address customBroadcaster = makeAddr("customBroadcaster");
|
|
39
|
+
|
|
40
|
+
(KozaGasToken token,) =
|
|
41
|
+
deployer.deploy("Custom Token", "CUST", 2_000_000 ether, 50_000 ether, customOwner, customBroadcaster);
|
|
42
|
+
|
|
43
|
+
assertEq(token.name(), "Custom Token");
|
|
44
|
+
assertEq(token.symbol(), "CUST");
|
|
45
|
+
assertEq(token.cap(), 2_000_000 ether);
|
|
46
|
+
assertEq(token.totalSupply(), 50_000 ether);
|
|
47
|
+
assertEq(token.owner(), customOwner);
|
|
48
|
+
assertEq(token.balanceOf(customOwner), 50_000 ether);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function test_Deploy_NoInitialMint() public {
|
|
52
|
+
DeployERC20Gas deployer = new DeployERC20Gas();
|
|
53
|
+
address broadcaster = makeAddr("broadcaster");
|
|
54
|
+
|
|
55
|
+
(KozaGasToken token,) = deployer.deploy("Koza Gas Token", "KGAS", 1_000_000 ether, 0, broadcaster, broadcaster);
|
|
56
|
+
|
|
57
|
+
assertEq(token.totalSupply(), 0);
|
|
58
|
+
assertEq(token.balanceOf(broadcaster), 0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.34;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {KozaGasToken} from "../src/KozaGasToken.sol";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @title ERC20GasHandler
|
|
9
|
+
* @notice Foundry invariant testing handler. Bounded random calls into KozaGasToken.
|
|
10
|
+
* @dev Handler pattern (Foundry stateful fuzzing) — invariant'ların ihlal edilmediği
|
|
11
|
+
* her tx sırasında doğrulanır. Owner pranking ile yönetim fonksiyonlarına erişim
|
|
12
|
+
* sağlanır.
|
|
13
|
+
*/
|
|
14
|
+
contract ERC20GasHandler is Test {
|
|
15
|
+
KozaGasToken public token;
|
|
16
|
+
address public owner;
|
|
17
|
+
address[] public actors;
|
|
18
|
+
|
|
19
|
+
// Mevcut tüm transfer/mint/burn aksiyon sayıları (debug için)
|
|
20
|
+
uint256 public callCount;
|
|
21
|
+
|
|
22
|
+
constructor(KozaGasToken _token, address _owner, address[] memory _actors) {
|
|
23
|
+
token = _token;
|
|
24
|
+
owner = _owner;
|
|
25
|
+
actors = _actors;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _pickActor(uint256 seed) internal view returns (address) {
|
|
29
|
+
return actors[seed % actors.length];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// @notice Bounded random transfer between actors.
|
|
33
|
+
function transfer(uint256 fromSeed, uint256 toSeed, uint256 amount) external {
|
|
34
|
+
callCount++;
|
|
35
|
+
address from = _pickActor(fromSeed);
|
|
36
|
+
address to = _pickActor(toSeed);
|
|
37
|
+
|
|
38
|
+
uint256 fromBalance = token.balanceOf(from);
|
|
39
|
+
if (fromBalance == 0) return;
|
|
40
|
+
|
|
41
|
+
amount = bound(amount, 1, fromBalance);
|
|
42
|
+
|
|
43
|
+
vm.prank(from);
|
|
44
|
+
try token.transfer(to, amount) returns (bool) {} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// @notice Bounded random mint by owner.
|
|
48
|
+
function mint(uint256 toSeed, uint256 amount) external {
|
|
49
|
+
callCount++;
|
|
50
|
+
address to = _pickActor(toSeed);
|
|
51
|
+
|
|
52
|
+
uint256 cap = token.cap();
|
|
53
|
+
uint256 totalSupply = token.totalSupply();
|
|
54
|
+
if (totalSupply >= cap) return;
|
|
55
|
+
|
|
56
|
+
// Avoid uint256 overflow by capping at type(uint128).max
|
|
57
|
+
amount = bound(amount, 1, cap - totalSupply);
|
|
58
|
+
|
|
59
|
+
vm.prank(owner);
|
|
60
|
+
try token.mint(to, amount) {} catch {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// @notice Bounded random burn by an actor.
|
|
64
|
+
function burn(uint256 actorSeed, uint256 amount) external {
|
|
65
|
+
callCount++;
|
|
66
|
+
address actor = _pickActor(actorSeed);
|
|
67
|
+
|
|
68
|
+
uint256 balance = token.balanceOf(actor);
|
|
69
|
+
if (balance == 0) return;
|
|
70
|
+
|
|
71
|
+
amount = bound(amount, 1, balance);
|
|
72
|
+
|
|
73
|
+
vm.prank(actor);
|
|
74
|
+
try token.burn(amount) {} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// @notice Returns the current actor list (used by invariant test).
|
|
78
|
+
function getActors() external view returns (address[] memory) {
|
|
79
|
+
return actors;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @title ERC20GasInvariantTest
|
|
85
|
+
* @notice Stateful fuzzing invariants for KozaGasToken. Foundry's invariant runner
|
|
86
|
+
* randomly calls handler functions and asserts these invariants hold after
|
|
87
|
+
* every call.
|
|
88
|
+
*
|
|
89
|
+
* Tested invariants:
|
|
90
|
+
* 1. totalSupply() <= cap() — never exceeds the cap
|
|
91
|
+
* 2. sum(balanceOf(actors)) == totalSupply() — balance accounting consistent
|
|
92
|
+
*/
|
|
93
|
+
contract ERC20GasInvariantTest is Test {
|
|
94
|
+
KozaGasToken internal token;
|
|
95
|
+
ERC20GasHandler internal handler;
|
|
96
|
+
address internal owner;
|
|
97
|
+
address[] internal actors;
|
|
98
|
+
|
|
99
|
+
uint256 internal constant CAP = 10_000_000 ether;
|
|
100
|
+
uint256 internal constant INITIAL_MINT = 1_000_000 ether;
|
|
101
|
+
|
|
102
|
+
function setUp() public {
|
|
103
|
+
owner = makeAddr("owner");
|
|
104
|
+
|
|
105
|
+
// Set up 5 actors and seed first one with the initial mint
|
|
106
|
+
actors.push(owner);
|
|
107
|
+
actors.push(makeAddr("alice"));
|
|
108
|
+
actors.push(makeAddr("bob"));
|
|
109
|
+
actors.push(makeAddr("charlie"));
|
|
110
|
+
actors.push(makeAddr("dave"));
|
|
111
|
+
|
|
112
|
+
token = new KozaGasToken("Koza Gas Token", "KGAS", CAP, INITIAL_MINT, owner);
|
|
113
|
+
handler = new ERC20GasHandler(token, owner, actors);
|
|
114
|
+
|
|
115
|
+
// Configure Foundry invariant runner to only call our handler
|
|
116
|
+
targetContract(address(handler));
|
|
117
|
+
|
|
118
|
+
// Restrict the function selectors that can be invoked
|
|
119
|
+
bytes4[] memory selectors = new bytes4[](3);
|
|
120
|
+
selectors[0] = handler.transfer.selector;
|
|
121
|
+
selectors[1] = handler.mint.selector;
|
|
122
|
+
selectors[2] = handler.burn.selector;
|
|
123
|
+
targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// @notice totalSupply must never exceed cap.
|
|
127
|
+
function invariant_TotalSupplyDoesNotExceedCap() public view {
|
|
128
|
+
assertLe(token.totalSupply(), token.cap(), "totalSupply > cap");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// @notice Sum of balances across all actors must equal totalSupply.
|
|
132
|
+
function invariant_SumOfBalancesEqualsTotalSupply() public view {
|
|
133
|
+
uint256 sum;
|
|
134
|
+
for (uint256 i = 0; i < actors.length; i++) {
|
|
135
|
+
sum += token.balanceOf(actors[i]);
|
|
136
|
+
}
|
|
137
|
+
assertEq(sum, token.totalSupply(), "sum(balances) != totalSupply");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// @notice Owner remains constant during invariant runs (no ownership change exposed).
|
|
141
|
+
function invariant_OwnerDoesNotChange() public view {
|
|
142
|
+
assertEq(token.owner(), owner, "owner mutated unexpectedly");
|
|
143
|
+
}
|
|
144
|
+
}
|