From 4336ddb159a50e12a355ea094a46786f8a483569 Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:50:03 -0500 Subject: [PATCH 01/11] Additional supply calculator functions --- foundry.lock | 17 ++ script/BaseDeployment.s.sol | 8 +- script/Config.s.sol | 16 +- script/Deploy.s.sol | 44 ++-- script/Update.s.sol | 194 ++++++++++++++++-- script/manage | 48 +++-- src/calculators/SupplyCalculator.sol | 166 +++++++++++++++ src/circulating/CirculatingZKC.sol | 76 ------- .../SupplyCalculator.t.sol} | 118 ++++++++--- update_deployment_toml.py | 20 +- 10 files changed, 533 insertions(+), 174 deletions(-) create mode 100644 foundry.lock create mode 100644 src/calculators/SupplyCalculator.sol delete mode 100644 src/circulating/CirculatingZKC.sol rename test/{circulating/CirculatingZKC.t.sol => calculators/SupplyCalculator.t.sol} (50%) diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..edb1427 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,17 @@ +{ + "lib/forge-std": { + "rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505" + }, + "lib/openzeppelin-contracts": { + "rev": "9393147db73ae6261b67cb03003370e9a7fa2448" + }, + "lib/openzeppelin-contracts-upgradeable": { + "rev": "a2c43a5aed2ac0153340c93ed452915b650d1921" + }, + "lib/openzeppelin-foundry-upgrades": { + "rev": "cfd861bc18ef4737e82eae6ec75304e27af699ef" + }, + "lib/prb-math": { + "rev": "dae52a48422f7464e7eeb4eea824f44dde7dfabe" + } +} \ No newline at end of file diff --git a/script/BaseDeployment.s.sol b/script/BaseDeployment.s.sol index 5af6601..5b2ad87 100644 --- a/script/BaseDeployment.s.sol +++ b/script/BaseDeployment.s.sol @@ -132,10 +132,10 @@ abstract contract BaseDeployment is Script { } /** - * @notice Updates the CirculatingZKC contract commit hash in deployment.toml via FFI + * @notice Updates the SupplyCalculator contract commit hash in deployment.toml via FFI * @param deploymentKey The chain key (e.g., "anvil", "ethereum-mainnet") */ - function _updateCirculatingZKCCommit(string memory deploymentKey) internal { + function _updateSupplyCalculatorCommit(string memory deploymentKey) internal { string[] memory args = new string[](4); args[0] = "git"; args[1] = "rev-parse"; @@ -145,13 +145,13 @@ abstract contract BaseDeployment is Script { bytes memory result = vm.ffi(args); string memory commit = string(result); - // Update deployment.toml with CirculatingZKC commit + // Update deployment.toml with SupplyCalculator commit string[] memory updateArgs = new string[](6); updateArgs[0] = "python3"; updateArgs[1] = "update_deployment_toml.py"; updateArgs[2] = "--chain-key"; updateArgs[3] = deploymentKey; - updateArgs[4] = "--circulating-zkc-commit"; + updateArgs[4] = "--supply-calculator-commit"; updateArgs[5] = commit; vm.ffi(updateArgs); diff --git a/script/Config.s.sol b/script/Config.s.sol index f9635ab..d2c43a0 100644 --- a/script/Config.s.sol +++ b/script/Config.s.sol @@ -28,13 +28,14 @@ struct DeploymentConfig { address stakingRewardsDeployer; address povwMinter; address stakingMinter; - address circulatingZKC; - address circulatingZKCImpl; - address circulatingZKCAdmin; + address supplyCalculator; + address supplyCalculatorImpl; + address supplyCalculatorAdmin; + address supplyCalculatorAdmin2; string zkcCommit; string veZKCCommit; string stakingRewardsCommit; - string circulatingZKCCommit; + string supplyCalculatorCommit; } library ConfigLoader { @@ -76,9 +77,10 @@ library ConfigLoader { _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".staking-rewards-deployer")); config.povwMinter = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".povw-minter")); config.stakingMinter = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".staking-minter")); - config.circulatingZKC = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".circulating-zkc")); - config.circulatingZKCImpl = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".circulating-zkc-impl")); - config.circulatingZKCAdmin = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".circulating-zkc-admin")); + config.supplyCalculator = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator")); + config.supplyCalculatorImpl = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-impl")); + config.supplyCalculatorAdmin = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-admin")); + config.supplyCalculatorAdmin2 = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-admin-2")); // Read per-contract deployment commits, default to empty string if not found string memory zkcCommitKey = string.concat(keyPrefix, ".zkc-commit"); diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 64bca9a..60a4c7d 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -5,7 +5,7 @@ import {console2} from "forge-std/Script.sol"; import {ZKC} from "../src/ZKC.sol"; import {veZKC} from "../src/veZKC.sol"; import {StakingRewards} from "../src/rewards/StakingRewards.sol"; -import {CirculatingZKC} from "../src/circulating/CirculatingZKC.sol"; +import {SupplyCalculator} from "../src/calculators/SupplyCalculator.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {ConfigLoader, DeploymentConfig} from "./Config.s.sol"; @@ -213,18 +213,18 @@ contract DeployStakingRewards is BaseDeployment { } /** - * Sample Usage for CirculatingZKC deployment: + * Sample Usage for SupplyCalculator deployment: * * export CHAIN_KEY="anvil" * export INITIAL_UNLOCKED="500000000000000000000000000" # 500M tokens * export SALT="0x0000000000000000000000000000000000000000000000000000000000000001" * - * forge script script/Deploy.s.sol:DeployCirculatingZKC \ + * forge script script/Deploy.s.sol:DeploySupplyCalculator \ * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ * --broadcast \ * --rpc-url http://127.0.0.1:8545 */ -contract DeployCirculatingZKC is BaseDeployment { +contract DeploySupplyCalculator is BaseDeployment { function setUp() public {} function run() public { @@ -241,35 +241,35 @@ contract DeployCirculatingZKC is BaseDeployment { bytes32 salt = vm.envOr("SALT", bytes32(0)); - // Deploy CirculatingZKC implementation - address circulatingZKCImpl = address(new CirculatingZKC{salt: salt}()); - console2.log("Deployed CirculatingZKC implementation to: ", circulatingZKCImpl); + // Deploy SupplyCalculator implementation + address supplyCalculatorImpl = address(new SupplyCalculator{salt: salt}()); + console2.log("Deployed SupplyCalculator implementation to: ", supplyCalculatorImpl); // Deploy proxy with initialization ERC1967Proxy proxy = new ERC1967Proxy{salt: salt}( - circulatingZKCImpl, abi.encodeCall(CirculatingZKC.initialize, (config.zkc, initialUnlockedRaw, admin)) + supplyCalculatorImpl, abi.encodeCall(SupplyCalculator.initialize, (config.zkc, initialUnlockedRaw, admin)) ); - address circulatingZKCAddress = address(proxy); + address supplyCalculatorAddress = address(proxy); vm.stopBroadcast(); // Update deployment.toml - _updateDeploymentConfig(deploymentKey, "circulating-zkc", circulatingZKCAddress); - _updateDeploymentConfig(deploymentKey, "circulating-zkc-impl", circulatingZKCImpl); - _updateDeploymentConfig(deploymentKey, "circulating-zkc-admin", admin); - _updateCirculatingZKCCommit(deploymentKey); + _updateDeploymentConfig(deploymentKey, "supply-calculator", supplyCalculatorAddress); + _updateDeploymentConfig(deploymentKey, "supply-calculator-impl", supplyCalculatorImpl); + _updateDeploymentConfig(deploymentKey, "supply-calculator-admin", admin); + _updateSupplyCalculatorCommit(deploymentKey); // Sanity checks - CirculatingZKC circulatingContract = CirculatingZKC(circulatingZKCAddress); - IAccessControl accessControl = IAccessControl(circulatingZKCAddress); + SupplyCalculator supplyCalculator = SupplyCalculator(supplyCalculatorAddress); + IAccessControl accessControl = IAccessControl(supplyCalculatorAddress); console2.log("Admin address: ", admin); - console2.log("Admin role assigned: ", accessControl.hasRole(circulatingContract.ADMIN_ROLE(), admin)); - console2.log("ZKC token address: ", address(circulatingContract.zkc())); - console2.log("Initial unlocked amount: ", circulatingContract.unlocked()); - console2.log("Initial unlocked amount (in tokens): ", circulatingContract.unlocked() / 10 ** 18); - console2.log("Current circulating supply: ", circulatingContract.circulatingSupply()); - console2.log("Current circulating supply (in tokens): ", circulatingContract.circulatingSupply() / 10 ** 18); + console2.log("Admin role assigned: ", accessControl.hasRole(supplyCalculator.ADMIN_ROLE(), admin)); + console2.log("ZKC token address: ", address(supplyCalculator.zkc())); + console2.log("Initial unlocked amount: ", supplyCalculator.unlocked()); + console2.log("Initial unlocked amount (in tokens): ", supplyCalculator.unlocked() / 10 ** 18); + console2.log("Current circulating supply: ", supplyCalculator.circulatingSupply()); + console2.log("Current circulating supply (in tokens): ", supplyCalculator.circulatingSupply() / 10 ** 18); console2.log("================================================"); - console2.log("Deployed CirculatingZKC to: ", circulatingZKCAddress); + console2.log("Deployed SupplyCalculator to: ", supplyCalculatorAddress); } } diff --git a/script/Update.s.sol b/script/Update.s.sol index 533c347..09dd140 100644 --- a/script/Update.s.sol +++ b/script/Update.s.sol @@ -6,7 +6,7 @@ import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol" import {ConfigLoader, DeploymentConfig} from "./Config.s.sol"; import {BaseDeployment} from "./BaseDeployment.s.sol"; import {ZKC} from "../src/ZKC.sol"; -import {CirculatingZKC} from "../src/circulating/CirculatingZKC.sol"; +import {SupplyCalculator} from "../src/calculators/SupplyCalculator.sol"; import {veZKC} from "../src/veZKC.sol"; import {StakingRewards} from "../src/rewards/StakingRewards.sol"; @@ -197,13 +197,13 @@ contract UpdateStakingMinter is BaseDeployment { } /** - * Sample Usage for updating CirculatingZKC unlocked value: + * Sample Usage for updating SupplyCalculator unlocked value: * * # Direct execution: * export CHAIN_KEY="anvil" * export NEW_UNLOCKED="750000000000000000000000000" # 750M tokens * - * forge script script/Update.s.sol:UpdateCirculatingUnlocked \ + * forge script script/Update.s.sol:UpdateSupplyCalculatorUnlocked \ * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ * --broadcast \ * --rpc-url http://127.0.0.1:8545 @@ -213,16 +213,16 @@ contract UpdateStakingMinter is BaseDeployment { * export NEW_UNLOCKED="750000000000000000000000000" # 750M tokens * export GNOSIS_EXECUTE=true * - * forge script script/Update.s.sol:UpdateCirculatingUnlocked \ + * forge script script/Update.s.sol:UpdateSupplyCalculatorUnlocked \ * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ * --rpc-url http://127.0.0.1:8545 */ -contract UpdateCirculatingUnlocked is BaseDeployment { +contract UpdateSupplyCalculatorUnlocked is BaseDeployment { function setUp() public {} function run() public { (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); - require(config.circulatingZKC != address(0), "CirculatingZKC address not set in deployment.toml"); + require(config.supplyCalculator != address(0), "SupplyCalculator address not set in deployment.toml"); // Get new unlocked value from environment uint256 newUnlocked = vm.envUint("NEW_UNLOCKED"); @@ -232,11 +232,11 @@ contract UpdateCirculatingUnlocked is BaseDeployment { bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); // Get the contract instance - CirculatingZKC circulatingContract = CirculatingZKC(config.circulatingZKC); + SupplyCalculator supplyCalculator = SupplyCalculator(config.supplyCalculator); // Get current values for logging - uint256 currentUnlocked = circulatingContract.unlocked(); - uint256 currentCirculatingSupply = circulatingContract.circulatingSupply(); + uint256 currentUnlocked = supplyCalculator.unlocked(); + uint256 currentCirculatingSupply = supplyCalculator.circulatingSupply(); console2.log("================================================"); console2.log("Current unlocked amount: ", currentUnlocked); @@ -247,7 +247,7 @@ contract UpdateCirculatingUnlocked is BaseDeployment { if (gnosisExecute) { // Print Gnosis Safe transaction info for manual execution - _printGnosisSafeInfo(config.circulatingZKC, newUnlocked); + _printGnosisSafeInfo(config.supplyCalculator, newUnlocked); // Calculate expected new circulating supply for display uint256 expectedNewCirculatingSupply = currentCirculatingSupply - currentUnlocked + newUnlocked; @@ -262,11 +262,11 @@ contract UpdateCirculatingUnlocked is BaseDeployment { vm.startBroadcast(); // Update the unlocked value - circulatingContract.updateUnlockedValue(newUnlocked); + supplyCalculator.updateUnlockedValue(newUnlocked); // Get updated values - uint256 updatedUnlocked = circulatingContract.unlocked(); - uint256 updatedCirculatingSupply = circulatingContract.circulatingSupply(); + uint256 updatedUnlocked = supplyCalculator.unlocked(); + uint256 updatedCirculatingSupply = supplyCalculator.circulatingSupply(); vm.stopBroadcast(); @@ -287,7 +287,7 @@ contract UpdateCirculatingUnlocked is BaseDeployment { } /// @notice Print Gnosis Safe transaction information for manual updates - /// @param targetAddress The CirculatingZKC contract address (target for Gnosis Safe) + /// @param targetAddress The SupplyCalculator contract address (target for Gnosis Safe) /// @param newUnlocked The new unlocked value to set function _printGnosisSafeInfo(address targetAddress, uint256 newUnlocked) internal pure { console2.log("================================"); @@ -912,6 +912,172 @@ contract RemoveStakingRewardsAdmin is BaseDeployment { } } +/** + * Sample Usage for adding admin to SupplyCalculator: + * + * export CHAIN_KEY="anvil" + * export ADMIN_TO_ADD="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + * + * forge script script/Update.s.sol:AddSupplyCalculatorAdmin \ + * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + * --broadcast \ + * --rpc-url http://127.0.0.1:8545 + */ +contract AddSupplyCalculatorAdmin is BaseDeployment { + function setUp() public {} + + function run() public { + (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); + require(config.supplyCalculator != address(0), "SupplyCalculator address not set in deployment.toml"); + + address adminToAdd = vm.envAddress("ADMIN_TO_ADD"); + require(adminToAdd != address(0), "ADMIN_TO_ADD environment variable not set"); + + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + SupplyCalculator supplyCalculatorContract = SupplyCalculator(config.supplyCalculator); + bytes32 adminRole = supplyCalculatorContract.ADMIN_ROLE(); + + if (gnosisExecute) { + console2.log("GNOSIS_EXECUTE=true: Preparing grantRole calldata for Safe execution"); + console2.log("SupplyCalculator Contract: ", config.supplyCalculator); + console2.log("Admin to Add: ", adminToAdd); + console2.log("Role: ADMIN_ROLE"); + + // Print Gnosis Safe transaction info for grantRole + bytes memory grantRoleCallData = + abi.encodeWithSignature("grantRole(bytes32,address)", adminRole, adminToAdd); + console2.log("================================"); + console2.log("=== GNOSIS SAFE GRANT ROLE INFO ==="); + console2.log("Target Address (To): ", config.supplyCalculator); + console2.log("Function: grantRole(bytes32,address)"); + console2.log("Role: "); + console2.logBytes32(adminRole); + console2.log("Account: ", adminToAdd); + console2.log("Calldata:"); + console2.logBytes(grantRoleCallData); + console2.log("====================================="); + console2.log("SupplyCalculator Admin Grant Role Calldata Ready"); + console2.log("Transaction NOT executed - use Gnosis Safe to execute"); + } else { + vm.startBroadcast(); + + IAccessControl accessControl = IAccessControl(config.supplyCalculator); + + // Check if caller has admin role + require( + accessControl.hasRole(supplyCalculatorContract.ADMIN_ROLE(), msg.sender), "Caller must have ADMIN_ROLE" + ); + + // Grant ADMIN_ROLE + accessControl.grantRole(adminRole, adminToAdd); + + vm.stopBroadcast(); + + // Sanity checks + console2.log("SupplyCalculator Contract: ", config.supplyCalculator); + console2.log("New SupplyCalculator Admin: ", adminToAdd); + console2.log("ADMIN_ROLE granted: ", accessControl.hasRole(adminRole, adminToAdd)); + console2.log("================================================"); + console2.log("SupplyCalculator Admin Role Updated Successfully"); + } + + // Update deployment.toml with the new admin (always do this) + if (config.supplyCalculatorAdmin2 == address(0) || config.supplyCalculatorAdmin2 == adminToAdd) { + _updateDeploymentConfig(deploymentKey, "supply-calculator-admin-2", adminToAdd); + } else if (config.supplyCalculatorAdmin == address(0) || config.supplyCalculatorAdmin == adminToAdd) { + _updateDeploymentConfig(deploymentKey, "supply-calculator-admin", adminToAdd); + } else { + revert("supply-calculator-admin-2 and supply-calculator-admin are both set already"); + } + } +} + +/** + * Sample Usage for removing admin from SupplyCalculator: + * + * export CHAIN_KEY="anvil" + * export ADMIN_TO_REMOVE="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + * + * forge script script/Update.s.sol:RemoveSupplyCalculatorAdmin \ + * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + * --broadcast \ + * --rpc-url http://127.0.0.1:8545 + */ +contract RemoveSupplyCalculatorAdmin is BaseDeployment { + function setUp() public {} + + function run() public { + (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); + require(config.supplyCalculator != address(0), "SupplyCalculator address not set in deployment.toml"); + + address adminToRemove = vm.envAddress("ADMIN_TO_REMOVE"); + require(adminToRemove != address(0), "ADMIN_TO_REMOVE environment variable not set"); + + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + SupplyCalculator supplyCalculatorContract = SupplyCalculator(config.supplyCalculator); + bytes32 adminRole = supplyCalculatorContract.ADMIN_ROLE(); + + // Safety check: Ensure at least one other admin will remain + IAccessControl accessControl = IAccessControl(config.supplyCalculator); + + address otherAdmin = (config.supplyCalculatorAdmin != address(0) && config.supplyCalculatorAdmin != adminToRemove) + ? config.supplyCalculatorAdmin + : config.supplyCalculatorAdmin2; + require( + otherAdmin != adminToRemove && otherAdmin != address(0) + && accessControl.hasRole(supplyCalculatorContract.ADMIN_ROLE(), otherAdmin), + "Cannot remove admin: would leave SupplyCalculator without any admins" + ); + + if (gnosisExecute) { + console2.log("GNOSIS_EXECUTE=true: Preparing revokeRole calldata for Safe execution"); + console2.log("SupplyCalculator Contract: ", config.supplyCalculator); + console2.log("Admin to Remove: ", adminToRemove); + console2.log("Other Admin still active: ", otherAdmin); + console2.log("Role: ADMIN_ROLE"); + + // Print Gnosis Safe transaction info for revokeRole + bytes memory revokeRoleCallData = + abi.encodeWithSignature("revokeRole(bytes32,address)", adminRole, adminToRemove); + console2.log("================================"); + console2.log("=== GNOSIS SAFE REVOKE ROLE INFO ==="); + console2.log("Target Address (To): ", config.supplyCalculator); + console2.log("Function: revokeRole(bytes32,address)"); + console2.log("Role: "); + console2.logBytes32(adminRole); + console2.log("Account: ", adminToRemove); + console2.log("Calldata:"); + console2.logBytes(revokeRoleCallData); + console2.log("====================================="); + console2.log("SupplyCalculator Admin Revoke Role Calldata Ready"); + console2.log("Transaction NOT executed - use Gnosis Safe to execute"); + } else { + vm.startBroadcast(); + + // Check if caller has admin role + require( + accessControl.hasRole(supplyCalculatorContract.ADMIN_ROLE(), msg.sender), "Caller must have ADMIN_ROLE" + ); + + // Revoke ADMIN_ROLE + accessControl.revokeRole(adminRole, adminToRemove); + + vm.stopBroadcast(); + + // Sanity checks + console2.log("SupplyCalculator Contract: ", config.supplyCalculator); + console2.log("Removed SupplyCalculator Admin: ", adminToRemove); + console2.log("Other Admin still active: ", otherAdmin); + console2.log("ADMIN_ROLE revoked: ", !accessControl.hasRole(adminRole, adminToRemove)); + console2.log("================================================"); + console2.log("SupplyCalculator Admin Role Removed Successfully"); + } + + // Remove from deployment.toml - check both admin fields + _removeAdminFromToml(deploymentKey, adminToRemove, "supply-calculator-admin", "supply-calculator-admin-2"); + } +} + /** * Sample Usage for adding admin to all contracts: * diff --git a/script/manage b/script/manage index 68dde9b..d626af3 100755 --- a/script/manage +++ b/script/manage @@ -257,7 +257,7 @@ Commands: deploy-zkc Deploy ZKC token contract deploy-vezkc Deploy veZKC staking contract deploy-staking Deploy StakingRewards contract - deploy-circulating Deploy CirculatingZKC contract + deploy-supply-calculator Deploy SupplyCalculator contract upgrade-zkc Upgrade ZKC implementation (no initializer) upgrade-zkc-initv2 Upgrade ZKC implementation and call initializeV2 zkc-start-epochs Call initializeV3 to start epochs (no upgrade) @@ -268,15 +268,17 @@ Commands: rollback-staking Rollback StakingRewards to previous implementation update-povw-minter Set POVW_MINTER_ROLE in ZKC contract update-staking-minter Set STAKING_MINTER_ROLE in ZKC contract - remove-povw-minter Revoke POVW_MINTER_ROLE from address in ZKC contract - update-circulating Update unlocked value in CirculatingZKC contract - add-zkc-admin Add admin to ZKC contract + remove-povw-minter Revoke POVW_MINTER_ROLE from address in ZKC contract + update-supply-calculator Update unlocked value in SupplyCalculator contract + add-zkc-admin Add admin to ZKC contract remove-zkc-admin Remove admin from ZKC contract add-vezkc-admin Add admin to veZKC contract remove-vezkc-admin Remove admin from veZKC contract - add-staking-admin Add admin to StakingRewards contract - remove-staking-admin Remove admin from StakingRewards contract - add-admin-all Add admin to all contracts (ZKC, veZKC, StakingRewards) + add-staking-admin Add admin to StakingRewards contract + remove-staking-admin Remove admin from StakingRewards contract + add-supply-calculator-admin Add admin to SupplyCalculator contract + remove-supply-calculator-admin Remove admin from SupplyCalculator contract + add-admin-all Add admin to all contracts (ZKC, veZKC, StakingRewards) remove-admin-all Remove admin from all contracts (ZKC, veZKC, StakingRewards) dev-mint-to-self Development: mint initial tokens to caller @@ -316,8 +318,8 @@ Examples: # Deploy StakingRewards to anvil (requires ZKC and veZKC already deployed) PRIVATE_KEY=0x... CHAIN_KEY=anvil ./script/manage deploy-staking --broadcast - # Deploy CirculatingZKC to anvil (requires ZKC already deployed) - PRIVATE_KEY=0x... INITIAL_UNLOCKED="500000000000000000000000000" CHAIN_KEY=anvil ./script/manage deploy-circulating --broadcast + # Deploy SupplyCalculator to anvil (requires ZKC already deployed) + PRIVATE_KEY=0x... INITIAL_UNLOCKED="500000000000000000000000000" CHAIN_KEY=anvil ./script/manage deploy-supply-calculator --broadcast # Upgrade ZKC on mainnet with verification (no initializer) PRIVATE_KEY=0x... CHAIN_KEY=ethereum-mainnet ./script/manage upgrade-zkc --broadcast --verify @@ -340,8 +342,8 @@ Examples: # Remove POVW minter role (requires POVW_MINTER env var) PRIVATE_KEY=0x... POVW_MINTER=0x123... CHAIN_KEY=anvil ./script/manage remove-povw-minter --broadcast - # Update CirculatingZKC unlocked value (requires NEW_UNLOCKED env var) - PRIVATE_KEY=0x... NEW_UNLOCKED="750000000000000000000000000" CHAIN_KEY=anvil ./script/manage update-circulating --broadcast + # Update SupplyCalculator unlocked value (requires NEW_UNLOCKED env var) + PRIVATE_KEY=0x... NEW_UNLOCKED="750000000000000000000000000" CHAIN_KEY=anvil ./script/manage update-supply-calculator --broadcast # Add admin to ZKC contract PRIVATE_KEY=0x... ADMIN_TO_ADD=0x123... CHAIN_KEY=anvil ./script/manage add-zkc-admin --broadcast @@ -351,6 +353,12 @@ Examples: # Add admin to all contracts at once PRIVATE_KEY=0x... ADMIN_TO_ADD=0x123... CHAIN_KEY=anvil ./script/manage add-admin-all --broadcast + + # Add admin to SupplyCalculator contract + PRIVATE_KEY=0x... ADMIN_TO_ADD=0x123... CHAIN_KEY=anvil ./script/manage add-supply-calculator-admin --broadcast + + # Remove admin from SupplyCalculator contract + PRIVATE_KEY=0x... ADMIN_TO_REMOVE=0x123... CHAIN_KEY=anvil ./script/manage remove-supply-calculator-admin --broadcast # Development: mint initial tokens to self (requires MINT_AMOUNT env var) PRIVATE_KEY=0x... MINT_AMOUNT=1000000000000000000000000 CHAIN_KEY=anvil ./script/manage dev-mint-to-self --broadcast @@ -377,8 +385,8 @@ Gnosis Safe Examples (GNOSIS_EXECUTE=true): # Generate admin management for all contracts at once GNOSIS_EXECUTE=true ADMIN_TO_ADD=0x123... CHAIN_KEY=ethereum-mainnet ./script/manage add-admin-all - # Generate CirculatingZKC update calldata for Gnosis Safe - GNOSIS_EXECUTE=true NEW_UNLOCKED="750000000000000000000000000" CHAIN_KEY=ethereum-mainnet ./script/manage update-circulating + # Generate SupplyCalculator update calldata for Gnosis Safe + GNOSIS_EXECUTE=true NEW_UNLOCKED="750000000000000000000000000" CHAIN_KEY=ethereum-mainnet ./script/manage update-supply-calculator # Generate role revoke calldata for Gnosis Safe GNOSIS_EXECUTE=true POVW_MINTER=0x123... CHAIN_KEY=ethereum-mainnet ./script/manage remove-povw-minter @@ -527,8 +535,8 @@ main() { deploy-staking) forge_script "DeployStakingRewards" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; - deploy-circulating) - forge_script "DeployCirculatingZKC" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" + deploy-supply-calculator) + forge_script "DeploySupplyCalculator" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; upgrade-zkc) forge_script "UpgradeZKC" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" @@ -563,8 +571,8 @@ main() { remove-povw-minter) forge_script "RemovePOVWMinter" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; - update-circulating) - forge_script "UpdateCirculatingUnlocked" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" + update-supply-calculator) + forge_script "UpdateSupplyCalculatorUnlocked" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; add-zkc-admin) forge_script "AddZKCAdmin" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" @@ -584,6 +592,12 @@ main() { remove-staking-admin) forge_script "RemoveStakingRewardsAdmin" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; + add-supply-calculator-admin) + forge_script "AddSupplyCalculatorAdmin" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" + ;; + remove-supply-calculator-admin) + forge_script "RemoveSupplyCalculatorAdmin" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" + ;; add-admin-all) forge_script "AddAdminAll" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; diff --git a/src/calculators/SupplyCalculator.sol b/src/calculators/SupplyCalculator.sol new file mode 100644 index 0000000..6664c7f --- /dev/null +++ b/src/calculators/SupplyCalculator.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IZKC} from "../interfaces/IZKC.sol"; +import {Supply} from "../libraries/Supply.sol"; + +/// @title Supply Calculator for ZKC tokens +/// @notice Contract computes various supply metrics of ZKC tokens, intended to be used by frontend apps/exchanges +contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgradeable { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + /// @notice Admin role identifier + bytes32 public immutable ADMIN_ROLE = DEFAULT_ADMIN_ROLE; + + /// @notice Reference to the ZKC token contract + IZKC public zkc; + + /// @notice Amount of tokens considered unlocked and in circulation + uint256 public unlocked; + + /// @notice Emitted when the unlocked value is updated + /// @param oldValue The previous unlocked value + /// @param newValue The new unlocked value + event UnlockedValueUpdated(uint256 oldValue, uint256 newValue); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the SupplyCalculator contract + /// @param _zkc Address of the ZKC token contract + /// @param _initialUnlocked Initial value for the unlocked tokens + /// @param _admin Address that will be granted the admin role + function initialize(address _zkc, uint256 _initialUnlocked, address _admin) public initializer { + require(_zkc != address(0), "ZKC address cannot be zero"); + require(_admin != address(0), "Admin address cannot be zero"); + + __AccessControl_init(); + __UUPSUpgradeable_init(); + + zkc = IZKC(_zkc); + unlocked = _initialUnlocked; + _grantRole(ADMIN_ROLE, _admin); + } + + /// @notice Calculate the current circulating supply + /// @dev Formula: unlocked + (zkc.claimedTotalSupply() - zkc.INITIAL_SUPPLY()) + /// @return The current circulating supply of ZKC tokens + function circulatingSupply() public view returns (uint256) { + uint256 claimedTotal = zkc.claimedTotalSupply(); + uint256 initialSupply = Supply.INITIAL_SUPPLY; + + // Calculate the claimed rewards from PoVW and staking rewards + uint256 claimedRewards = claimedTotal - initialSupply; + + return unlocked + claimedRewards; + } + + /// @notice Calculate the current circulating supply rounded to the nearest whole token (18dp representation) + /// @dev Returns value in wei (18 decimals) but rounded such that when converted to whole tokens it's rounded + /// @return The current circulating supply rounded to nearest whole token in 18dp format + function circulatingSupplyRounded() public view returns (uint256) { + uint256 supply = circulatingSupply(); + return _roundTo18dp(supply); + } + + /// @notice Calculate the current circulating supply as a rounded whole number + /// @dev Returns the value in regular representation (divided by 10^18) rounded to nearest whole token + /// @return The current circulating supply as a whole number + function circulatingSupplyAmountRounded() public view returns (uint256) { + uint256 supply = circulatingSupply(); + return _roundToWholeTokens(supply); + } + + function totalSupply() public view returns (uint256) { + return IERC20(address(zkc)).totalSupply(); + } + + /// @notice Get the total supply rounded to the nearest whole token (18dp representation) + /// @dev Returns value in wei (18 decimals) but rounded such that when converted to whole tokens it's rounded + /// @dev Uses the theoretical total supply based on current epoch + /// @return The total supply rounded to nearest whole token in 18dp format + function totalSupplyRounded() public view returns (uint256) { + uint256 supply = IERC20(address(zkc)).totalSupply(); + return _roundTo18dp(supply); + } + + /// @notice Get the total supply as a rounded whole number + /// @dev Returns the value in regular representation (divided by 10^18) rounded to nearest whole token + /// @dev Uses the theoretical total supply based on current epoch + /// @return The total supply as a whole number + function totalSupplyAmountRounded() public view returns (uint256) { + uint256 supply = IERC20(address(zkc)).totalSupply(); + return _roundToWholeTokens(supply); + } + + function totalClaimedSupply() public view returns (uint256) { + return zkc.claimedTotalSupply(); + } + + /// @notice Get the claimed total supply rounded to the nearest whole token (18dp representation) + /// @dev Returns value in wei (18 decimals) but rounded such that when converted to whole tokens it's rounded + /// @dev Uses the actual claimed/minted supply + /// @return The claimed total supply rounded to nearest whole token in 18dp format + function totalClaimedSupplyRounded() public view returns (uint256) { + uint256 supply = zkc.claimedTotalSupply(); + return _roundTo18dp(supply); + } + + /// @notice Get the claimed total supply as a rounded whole number + /// @dev Returns the value in regular representation (divided by 10^18) rounded to nearest whole token + /// @dev Uses the actual claimed/minted supply + /// @return The claimed total supply as a whole number + function totalClaimedSupplyAmountRounded() public view returns (uint256) { + uint256 supply = zkc.claimedTotalSupply(); + return _roundToWholeTokens(supply); + } + + /// @notice Round a value to the nearest whole token (18dp representation) + /// @dev Rounds to nearest 1e18, so when divided by 1e18 it gives a whole number + /// @param value The value to round + /// @return The rounded value in 18dp format + function _roundTo18dp(uint256 value) private pure returns (uint256) { + uint256 remainder = value % 1e18; + if (remainder >= 5e17) { + // Round up + return value - remainder + 1e18; + } else { + // Round down + return value - remainder; + } + } + + /// @notice Round a value to the nearest whole token and return as a whole number + /// @dev Divides by 1e18 with rounding + /// @param value The value to round + /// @return The rounded value as a whole number + function _roundToWholeTokens(uint256 value) private pure returns (uint256) { + uint256 remainder = value % 1e18; + if (remainder >= 5e17) { + // Round up + return (value / 1e18) + 1; + } else { + // Round down + return value / 1e18; + } + } + + /// @notice Update the unlocked value + /// @dev Only callable by accounts with ADMIN_ROLE + /// @param _newUnlocked The new value for unlocked tokens + function updateUnlockedValue(uint256 _newUnlocked) external onlyRole(ADMIN_ROLE) { + uint256 oldValue = unlocked; + unlocked = _newUnlocked; + emit UnlockedValueUpdated(oldValue, _newUnlocked); + } + + /// @notice Authorize contract upgrades (UUPS pattern) + /// @dev Only accounts with ADMIN_ROLE can authorize upgrades + /// @param newImplementation Address of the new implementation contract + function _authorizeUpgrade(address newImplementation) internal override onlyRole(ADMIN_ROLE) {} +} diff --git a/src/circulating/CirculatingZKC.sol b/src/circulating/CirculatingZKC.sol deleted file mode 100644 index 4a4b7d1..0000000 --- a/src/circulating/CirculatingZKC.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {IZKC} from "../interfaces/IZKC.sol"; -import {Supply} from "../libraries/Supply.sol"; - -/// @title CirculatingZKC - Circulating Supply Calculator for ZKC -/// @notice Contract to track and calculate the circulating supply of ZKC tokens -/// @dev Uses an admin-controlled unlocked value plus claimed tokens to calculate circulating supply -contract CirculatingZKC is Initializable, AccessControlUpgradeable, UUPSUpgradeable { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - /// @notice Admin role identifier - bytes32 public immutable ADMIN_ROLE = DEFAULT_ADMIN_ROLE; - - /// @notice Reference to the ZKC token contract - IZKC public zkc; - - /// @notice Amount of tokens considered unlocked and in circulation - uint256 public unlocked; - - /// @notice Emitted when the unlocked value is updated - /// @param oldValue The previous unlocked value - /// @param newValue The new unlocked value - event UnlockedValueUpdated(uint256 oldValue, uint256 newValue); - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialize the CirculatingZKC contract - /// @param _zkc Address of the ZKC token contract - /// @param _initialUnlocked Initial value for the unlocked tokens - /// @param _admin Address that will be granted the admin role - function initialize(address _zkc, uint256 _initialUnlocked, address _admin) public initializer { - require(_zkc != address(0), "ZKC address cannot be zero"); - require(_admin != address(0), "Admin address cannot be zero"); - - __AccessControl_init(); - __UUPSUpgradeable_init(); - - zkc = IZKC(_zkc); - unlocked = _initialUnlocked; - _grantRole(ADMIN_ROLE, _admin); - } - - /// @notice Calculate the current circulating supply - /// @dev Formula: unlocked + (zkc.claimedTotalSupply() - zkc.INITIAL_SUPPLY()) - /// @return The current circulating supply of ZKC tokens - function circulatingSupply() public view returns (uint256) { - uint256 claimedTotal = zkc.claimedTotalSupply(); - uint256 initialSupply = Supply.INITIAL_SUPPLY; - - // Calculate the claimed rewards from PoVW and staking rewards - uint256 claimedRewards = claimedTotal - initialSupply; - - return unlocked + claimedRewards; - } - - /// @notice Update the unlocked value - /// @dev Only callable by accounts with ADMIN_ROLE - /// @param _newUnlocked The new value for unlocked tokens - function updateUnlockedValue(uint256 _newUnlocked) external onlyRole(ADMIN_ROLE) { - uint256 oldValue = unlocked; - unlocked = _newUnlocked; - emit UnlockedValueUpdated(oldValue, _newUnlocked); - } - - /// @notice Authorize contract upgrades (UUPS pattern) - /// @dev Only accounts with ADMIN_ROLE can authorize upgrades - /// @param newImplementation Address of the new implementation contract - function _authorizeUpgrade(address newImplementation) internal override onlyRole(ADMIN_ROLE) {} -} diff --git a/test/circulating/CirculatingZKC.t.sol b/test/calculators/SupplyCalculator.t.sol similarity index 50% rename from test/circulating/CirculatingZKC.t.sol rename to test/calculators/SupplyCalculator.t.sol index 70a664c..334f5a9 100644 --- a/test/circulating/CirculatingZKC.t.sol +++ b/test/calculators/SupplyCalculator.t.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; -import "../../src/circulating/CirculatingZKC.sol"; +import {SupplyCalculator} from "../../src/calculators/SupplyCalculator.sol"; import "../../src/ZKC.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -contract CirculatingZKCTest is Test { - CirculatingZKC public circulatingZKC; +contract SupplyCalculatorTest is Test { + SupplyCalculator public supplyCalculator; ZKC public zkc; address public owner = makeAddr("owner"); @@ -23,8 +23,8 @@ contract CirculatingZKCTest is Test { // Deploy ZKC deployZKC(); - // Deploy CirculatingZKC - deployCirculatingZKC(); + // Deploy SupplyCalculator + deploySupplyCalculator(); } function deployZKC() internal { @@ -60,27 +60,27 @@ contract CirculatingZKCTest is Test { vm.stopPrank(); } - function deployCirculatingZKC() internal { + function deploySupplyCalculator() internal { // Deploy implementation - CirculatingZKC implementation = new CirculatingZKC(); + SupplyCalculator implementation = new SupplyCalculator(); // Deploy proxy and initialize bytes memory initData = - abi.encodeWithSelector(CirculatingZKC.initialize.selector, address(zkc), INITIAL_UNLOCKED, owner); + abi.encodeWithSelector(SupplyCalculator.initialize.selector, address(zkc), INITIAL_UNLOCKED, owner); ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); - circulatingZKC = CirculatingZKC(address(proxy)); + supplyCalculator = SupplyCalculator(address(proxy)); } function testInitialization() public view { - assertEq(address(circulatingZKC.zkc()), address(zkc)); - assertEq(circulatingZKC.unlocked(), INITIAL_UNLOCKED); - assertTrue(circulatingZKC.hasRole(circulatingZKC.ADMIN_ROLE(), owner)); + assertEq(address(supplyCalculator.zkc()), address(zkc)); + assertEq(supplyCalculator.unlocked(), INITIAL_UNLOCKED); + assertTrue(supplyCalculator.hasRole(supplyCalculator.ADMIN_ROLE(), owner)); } function testCirculatingSupplyAfterInitialMint() public { // Circulating supply should be just unlocked since total minted (1B) - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); + uint256 circulatingSupply = supplyCalculator.circulatingSupply(); assertEq(circulatingSupply, INITIAL_UNLOCKED); } @@ -99,7 +99,7 @@ contract CirculatingZKCTest is Test { zkc.mintStakingRewardsForRecipient(user, stakingRewards); uint256 expectedCirculating = INITIAL_UNLOCKED + povwRewards + stakingRewards; - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); + uint256 circulatingSupply = supplyCalculator.circulatingSupply(); assertEq(circulatingSupply, expectedCirculating); } @@ -107,14 +107,14 @@ contract CirculatingZKCTest is Test { uint256 newUnlocked = 750_000_000e18; // 750M tokens vm.expectEmit(true, true, true, true); - emit CirculatingZKC.UnlockedValueUpdated(INITIAL_UNLOCKED, newUnlocked); + emit SupplyCalculator.UnlockedValueUpdated(INITIAL_UNLOCKED, newUnlocked); vm.prank(owner); - circulatingZKC.updateUnlockedValue(newUnlocked); + supplyCalculator.updateUnlockedValue(newUnlocked); - assertEq(circulatingZKC.unlocked(), newUnlocked); + assertEq(supplyCalculator.unlocked(), newUnlocked); // Check circulating supply updated correctly - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); + uint256 circulatingSupply = supplyCalculator.circulatingSupply(); assertEq(circulatingSupply, newUnlocked); } @@ -124,26 +124,26 @@ contract CirculatingZKCTest is Test { // Non-admin should not be able to update vm.prank(user); vm.expectRevert(); - circulatingZKC.updateUnlockedValue(newUnlocked); + supplyCalculator.updateUnlockedValue(newUnlocked); // Admin should be able to update vm.prank(owner); - circulatingZKC.updateUnlockedValue(newUnlocked); - assertEq(circulatingZKC.unlocked(), newUnlocked); + supplyCalculator.updateUnlockedValue(newUnlocked); + assertEq(supplyCalculator.unlocked(), newUnlocked); } function testUpgradeAccessControl() public { // Deploy new implementation - CirculatingZKC newImplementation = new CirculatingZKC(); + SupplyCalculator newImplementation = new SupplyCalculator(); // Non-admin should not be able to upgrade vm.prank(user); vm.expectRevert(); - circulatingZKC.upgradeToAndCall(address(newImplementation), ""); + supplyCalculator.upgradeToAndCall(address(newImplementation), ""); // Admin should be able to upgrade vm.prank(owner); - circulatingZKC.upgradeToAndCall(address(newImplementation), ""); + supplyCalculator.upgradeToAndCall(address(newImplementation), ""); } function testCirculatingSupplyWithBurnedTokens() public { @@ -161,7 +161,75 @@ contract CirculatingZKCTest is Test { zkc.burn(burnAmount); uint256 expectedCirculating = INITIAL_UNLOCKED + rewards - burnAmount; - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); + uint256 circulatingSupply = supplyCalculator.circulatingSupply(); assertEq(circulatingSupply, expectedCirculating); } + + function testCirculatingSupplyRounded() public { + // Test with value that rounds down + uint256 valueRoundDown = 500_000_000e18 + 0.3e18; + vm.prank(owner); + supplyCalculator.updateUnlockedValue(valueRoundDown); + + uint256 rounded18dp = supplyCalculator.circulatingSupplyRounded(); + assertEq(rounded18dp, 500_000_000e18); + + uint256 roundedAmount = supplyCalculator.circulatingSupplyAmountRounded(); + assertEq(roundedAmount, 500_000_000); + + // Test with value that rounds up + uint256 valueRoundUp = 500_000_000e18 + 0.7e18; + vm.prank(owner); + supplyCalculator.updateUnlockedValue(valueRoundUp); + + rounded18dp = supplyCalculator.circulatingSupplyRounded(); + assertEq(rounded18dp, 500_000_001e18); + + roundedAmount = supplyCalculator.circulatingSupplyAmountRounded(); + assertEq(roundedAmount, 500_000_001); + } + + function testTotalSupplyRounded() public { + // Skip forward to start epochs + uint256 epoch17TotalSupply = zkc.getSupplyAtEpochStart(17); + uint256 epoch17ExpectedTotalSupply = 1006339775710604115000000000; + assertEq(epoch17TotalSupply, epoch17ExpectedTotalSupply); + + vm.warp(block.timestamp + 17 * zkc.EPOCH_DURATION()); + + // Get the theoretical total supply + uint256 totalSupply = zkc.totalSupply(); + console.logUint(totalSupply); + console.logUint(epoch17TotalSupply); + assertEq(totalSupply, epoch17TotalSupply); + + // Get rounded values + uint256 rounded18dp = supplyCalculator.totalSupplyRounded(); + uint256 roundedAmount = supplyCalculator.totalSupplyAmountRounded(); + + // Verify rounding is correct + assertEq(rounded18dp, 1006339776000000000000000000); + assertEq(roundedAmount, 1006339776); + } + + function testTotalClaimedSupplyRounded() public { + // Skip forward in time to simulate epochs passing + vm.warp(block.timestamp + 4 weeks); + + // Mint some rewards to create a claimed supply > initial supply + uint256 rewards = 1_234_567.89e18; + vm.prank(povwMinter); + zkc.mintPoVWRewardsForRecipient(user, rewards); + + // Get the claimed total supply + uint256 claimedSupply = zkc.claimedTotalSupply(); + assertEq(claimedSupply, zkc.INITIAL_SUPPLY() + rewards); + + // Get rounded values + uint256 rounded18dp = supplyCalculator.totalClaimedSupplyRounded(); + uint256 roundedAmount = supplyCalculator.totalClaimedSupplyAmountRounded(); + + assertEq(rounded18dp, 1001234568000000000000000000); + assertEq(roundedAmount, 1001234568); + } } diff --git a/update_deployment_toml.py b/update_deployment_toml.py index 4863aa2..7fa9502 100755 --- a/update_deployment_toml.py +++ b/update_deployment_toml.py @@ -32,7 +32,8 @@ def main(): parser.add_argument('--vezkc-admin-2', help='veZKC secondary admin address') parser.add_argument('--staking-rewards-admin', help='StakingRewards admin address') parser.add_argument('--staking-rewards-admin-2', help='StakingRewards secondary admin address') - + parser.add_argument('--supply-calculator-admin-2', help='SupplyCalculator secondary admin address') + parser.add_argument('--zkc', help='ZKC proxy address') parser.add_argument('--zkc-impl', help='ZKC implementation address') parser.add_argument('--zkc-impl-prev', help='Previous ZKC implementation address') @@ -47,15 +48,15 @@ def main(): parser.add_argument('--staking-rewards-deployer', help='StakingRewards deployer address') parser.add_argument('--povw-minter', help='POVW minter address') parser.add_argument('--staking-minter', help='Staking minter address') - parser.add_argument('--circulating-zkc', help='CirculatingZKC proxy address') - parser.add_argument('--circulating-zkc-impl', help='CirculatingZKC implementation address') - parser.add_argument('--circulating-zkc-admin', help='CirculatingZKC admin address') + parser.add_argument('--supply-calculator', help='SupplyCalculator proxy address') + parser.add_argument('--supply-calculator-impl', help='SupplyCalculator implementation address') + parser.add_argument('--supply-calculator-admin', help='SupplyCalculator admin address') # Deployment metadata parser.add_argument('--zkc-commit', help='Git commit hash for ZKC deployment') parser.add_argument('--vezkc-commit', help='Git commit hash for veZKC deployment') parser.add_argument('--staking-rewards-commit', help='Git commit hash for StakingRewards deployment') - parser.add_argument('--circulating-zkc-commit', help='Git commit hash for CirculatingZKC deployment') + parser.add_argument('--supply-calculator-commit', help='Git commit hash for SupplyCalculator deployment') parser.add_argument('--rpc-url', help='RPC URL for the network') parser.add_argument('--etherscan-api-key', help='Etherscan API key') @@ -101,6 +102,7 @@ def main(): 'vezkc_admin_2': args.vezkc_admin_2, 'staking_rewards_admin': args.staking_rewards_admin, 'staking_rewards_admin_2': args.staking_rewards_admin_2, + 'supply_calculator_admin_2': args.supply_calculator_admin_2, 'zkc': args.zkc, 'zkc_impl': args.zkc_impl, 'zkc_impl_prev': args.zkc_impl_prev, @@ -115,13 +117,13 @@ def main(): 'staking_rewards_deployer': args.staking_rewards_deployer, 'povw_minter': args.povw_minter, 'staking_minter': args.staking_minter, - 'circulating_zkc': args.circulating_zkc, - 'circulating_zkc_impl': args.circulating_zkc_impl, - 'circulating_zkc_admin': args.circulating_zkc_admin, + 'supply_calculator': args.supply_calculator, + 'supply_calculator_impl': args.supply_calculator_impl, + 'supply_calculator_admin': args.supply_calculator_admin, 'zkc_commit': args.zkc_commit, 'vezkc_commit': args.vezkc_commit, 'staking_rewards_commit': args.staking_rewards_commit, - 'circulating_zkc_commit': args.circulating_zkc_commit, + 'supply_calculator_commit': args.supply_calculator_commit, 'rpc_url': args.rpc_url, 'etherscan_api_key': args.etherscan_api_key, } From 14facdc959285a45c3d34094616c64d804eb3b36 Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:04:59 -0500 Subject: [PATCH 02/11] Add calculator upgrade script --- script/Config.s.sol | 5 +++ script/Rollback.s.sol | 60 ++++++++++++++++++++++++++++++++ script/Upgrade.s.sol | 80 +++++++++++++++++++++++++++++++++++++++++++ script/manage | 26 ++++++++++++-- 4 files changed, 168 insertions(+), 3 deletions(-) diff --git a/script/Config.s.sol b/script/Config.s.sol index d2c43a0..24b7dd3 100644 --- a/script/Config.s.sol +++ b/script/Config.s.sol @@ -30,6 +30,7 @@ struct DeploymentConfig { address stakingMinter; address supplyCalculator; address supplyCalculatorImpl; + address supplyCalculatorImplPrev; address supplyCalculatorAdmin; address supplyCalculatorAdmin2; string zkcCommit; @@ -79,6 +80,7 @@ library ConfigLoader { config.stakingMinter = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".staking-minter")); config.supplyCalculator = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator")); config.supplyCalculatorImpl = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-impl")); + config.supplyCalculatorImplPrev = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-impl-prev")); config.supplyCalculatorAdmin = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-admin")); config.supplyCalculatorAdmin2 = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-admin-2")); @@ -92,6 +94,9 @@ library ConfigLoader { string memory stakingRewardsCommitKey = string.concat(keyPrefix, ".staking-rewards-commit"); config.stakingRewardsCommit = _readStringOrEmpty(toml, stakingRewardsCommitKey); + string memory supplyCalculatorCommitKey = string.concat(keyPrefix, ".supply-calculator-commit"); + config.supplyCalculatorCommit = _readStringOrEmpty(toml, supplyCalculatorCommitKey); + return (config, deploymentKey); } diff --git a/script/Rollback.s.sol b/script/Rollback.s.sol index 68c1ff8..2d1f002 100644 --- a/script/Rollback.s.sol +++ b/script/Rollback.s.sol @@ -8,6 +8,7 @@ import {BaseDeployment} from "./BaseDeployment.s.sol"; import {ZKC} from "../src/ZKC.sol"; import {veZKC} from "../src/veZKC.sol"; import {StakingRewards} from "../src/rewards/StakingRewards.sol"; +import {SupplyCalculator} from "../src/calculators/SupplyCalculator.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; /** @@ -206,3 +207,62 @@ contract RollbackStakingRewards is BaseDeployment { console2.log("Rolled back to implementation: ", rolledBackImpl); } } + +/** + * Sample Usage for SupplyCalculator rollback: + * + * export CHAIN_KEY="anvil" + * forge script script/Rollback.s.sol:RollbackSupplyCalculator \ + * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + * --broadcast \ + * --rpc-url http://127.0.0.1:8545 + */ +contract RollbackSupplyCalculator is BaseDeployment { + function run() public { + (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); + require(config.supplyCalculator != address(0), "SupplyCalculator not deployed"); + require( + config.supplyCalculatorImplPrev != address(0), "No previous SupplyCalculator implementation found for rollback" + ); + + vm.startBroadcast(); + + address currentImpl = _getImplementationAddress(config.supplyCalculator); + console2.log("Rolling back SupplyCalculator at: ", config.supplyCalculator); + console2.log("Current implementation: ", currentImpl); + console2.log("Previous implementation: ", config.supplyCalculatorImplPrev); + + // Verify previous implementation has code + require(_getCodeSize(config.supplyCalculatorImplPrev) > 0, "Previous implementation has no code"); + + // Perform rollback by directly upgrading to previous implementation (unsafe) + (bool success,) = config.supplyCalculator.call( + abi.encodeWithSignature("upgradeToAndCall(address,bytes)", config.supplyCalculatorImplPrev, "") + ); + require(success, "Failed to rollback SupplyCalculator implementation"); + + address rolledBackImpl = _getImplementationAddress(config.supplyCalculator); + console2.log("Rolled back SupplyCalculator implementation to: ", rolledBackImpl); + require(rolledBackImpl == config.supplyCalculatorImplPrev, "Rollback failed: implementation mismatch"); + + vm.stopBroadcast(); + + // Update deployment.toml: swap current and previous implementations + _updateDeploymentConfig(deploymentKey, "supply-calculator-impl", config.supplyCalculatorImplPrev); + _updateDeploymentConfig(deploymentKey, "supply-calculator-impl-prev", currentImpl); + + // Verify rollback + SupplyCalculator supplyCalculatorContract = SupplyCalculator(config.supplyCalculator); + IAccessControl accessControl = IAccessControl(config.supplyCalculator); + console2.log("Proxy still points to SupplyCalculator: ", address(supplyCalculatorContract) == config.supplyCalculator); + console2.log( + "Admin role still assigned: ", + accessControl.hasRole(supplyCalculatorContract.ADMIN_ROLE(), config.supplyCalculatorAdmin) + ); + console2.log("ZKC token still configured: ", address(supplyCalculatorContract.zkc()) == config.zkc); + console2.log("Rollback verification successful"); + console2.log("================================================"); + console2.log("SupplyCalculator Rollback Complete"); + console2.log("Rolled back to implementation: ", rolledBackImpl); + } +} diff --git a/script/Upgrade.s.sol b/script/Upgrade.s.sol index 769daa2..51ba583 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -8,6 +8,7 @@ import {BaseDeployment} from "./BaseDeployment.s.sol"; import {ZKC} from "../src/ZKC.sol"; import {veZKC} from "../src/veZKC.sol"; import {StakingRewards} from "../src/rewards/StakingRewards.sol"; +import {SupplyCalculator} from "../src/calculators/SupplyCalculator.sol"; /** * Sample Usage for ZKC upgrade: @@ -484,3 +485,82 @@ contract UpgradeStakingRewards is BaseDeployment { console2.log("New Implementation: ", newImpl); } } + +/** + * Sample Usage for SupplyCalculator upgrade: + * + * # First, create reference build from deployed SupplyCalculator commit: + * export DEPLOYED_COMMIT=$(python3 -c "import tomlkit; print(tomlkit.load(open('deployment.toml'))['deployment']['$CHAIN_KEY']['supply-calculator-commit'])") + * WORKTREE_PATH="../supply-calculator-reference-${DEPLOYED_COMMIT}" + * git worktree add "$WORKTREE_PATH" "$DEPLOYED_COMMIT" + * cd "$WORKTREE_PATH" + * forge build --profile reference + * cp -R out-reference/build-info "$OLDPWD/build-info-reference" + * cd "$OLDPWD" + * + * # Then run upgrade: + * export CHAIN_KEY="anvil" + * forge script script/Upgrade.s.sol:UpgradeSupplyCalculator \ + * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + * --broadcast \ + * --rpc-url http://127.0.0.1:8545 + */ +contract UpgradeSupplyCalculator is BaseDeployment { + function run() public { + (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); + require(config.supplyCalculator != address(0), "SupplyCalculator not deployed"); + + vm.startBroadcast(); + + // Check for skip safety checks flag + bool skipSafetyChecks = vm.envOr("SKIP_SAFETY_CHECKS", false); + + // Prepare upgrade options with reference contract + Options memory opts; + + if (skipSafetyChecks) { + console2.log("WARNING: Skipping all upgrade safety checks and reference build!"); + opts.unsafeSkipAllChecks = true; + } else { + // Get the SupplyCalculator commit hash from deployment config for commit-specific reference build + string memory supplyCalculatorCommit = config.supplyCalculatorCommit; + require(bytes(supplyCalculatorCommit).length > 0, "SupplyCalculator commit hash not found in deployment config"); + + string memory referenceBuildDir = string.concat("build-info-reference-", supplyCalculatorCommit); + opts.referenceContract = string.concat(referenceBuildDir, ":SupplyCalculator"); + opts.referenceBuildInfoDir = referenceBuildDir; + console2.log("Using reference build directory: ", referenceBuildDir); + } + + console2.log("Upgrading SupplyCalculator at: ", config.supplyCalculator); + address currentImpl = _getImplementationAddress(config.supplyCalculator); + console2.log("Current implementation: ", currentImpl); + + // Perform safe upgrade + Upgrades.upgradeProxy( + config.supplyCalculator, + "SupplyCalculator.sol:SupplyCalculator", + "", // No reinitializer + opts + ); + + address newImpl = Upgrades.getImplementationAddress(config.supplyCalculator); + console2.log("Upgraded SupplyCalculator implementation to: ", newImpl); + + vm.stopBroadcast(); + + // Update deployment.toml with new implementation and store previous + _updateDeploymentConfig(deploymentKey, "supply-calculator-impl-prev", currentImpl); + _updateDeploymentConfig(deploymentKey, "supply-calculator-impl", newImpl); + _updateSupplyCalculatorCommit(deploymentKey); + + // Verify upgrade + SupplyCalculator supplyCalculatorContract = SupplyCalculator(config.supplyCalculator); + console2.log("Proxy still points to SupplyCalculator: ", address(supplyCalculatorContract) == config.supplyCalculator); + console2.log("Implementation updated: ", newImpl != config.supplyCalculatorImpl); + console2.log("ZKC token still configured: ", address(supplyCalculatorContract.zkc()) == config.zkc); + console2.log("================================================"); + console2.log("SupplyCalculator Upgrade Complete"); + console2.log("New Implementation: ", newImpl); + } +} diff --git a/script/manage b/script/manage index d626af3..52711ef 100755 --- a/script/manage +++ b/script/manage @@ -81,6 +81,9 @@ create_reference_build() { staking) commit_field="staking-rewards-commit" ;; + supply-calculator) + commit_field="supply-calculator-commit" + ;; *) echo "❌ Unknown contract type: $contract_type" exit 1 @@ -261,11 +264,13 @@ Commands: upgrade-zkc Upgrade ZKC implementation (no initializer) upgrade-zkc-initv2 Upgrade ZKC implementation and call initializeV2 zkc-start-epochs Call initializeV3 to start epochs (no upgrade) - upgrade-vezkc Upgrade veZKC implementation + upgrade-vezkc Upgrade veZKC implementation upgrade-staking Upgrade StakingRewards implementation + upgrade-supply-calculator Upgrade SupplyCalculator implementation rollback-zkc Rollback ZKC to previous implementation rollback-vezkc Rollback veZKC to previous implementation rollback-staking Rollback StakingRewards to previous implementation + rollback-supply-calculator Rollback SupplyCalculator to previous implementation update-povw-minter Set POVW_MINTER_ROLE in ZKC contract update-staking-minter Set STAKING_MINTER_ROLE in ZKC contract remove-povw-minter Revoke POVW_MINTER_ROLE from address in ZKC contract @@ -332,10 +337,16 @@ Examples: # Upgrade ZKC skipping safety checks (WARNING: unsafe!) PRIVATE_KEY=0x... CHAIN_KEY=anvil SKIP_SAFETY_CHECKS=true ./script/manage upgrade-zkc --broadcast - + + # Upgrade SupplyCalculator on mainnet with verification + PRIVATE_KEY=0x... CHAIN_KEY=ethereum-mainnet ./script/manage upgrade-supply-calculator --broadcast --verify + # Rollback ZKC to previous implementation PRIVATE_KEY=0x... CHAIN_KEY=anvil ./script/manage rollback-zkc --broadcast - + + # Rollback SupplyCalculator to previous implementation + PRIVATE_KEY=0x... CHAIN_KEY=anvil ./script/manage rollback-supply-calculator --broadcast + # Set POVW minter role (requires POVW_MINTER env var) PRIVATE_KEY=0x... POVW_MINTER=0x123... CHAIN_KEY=anvil ./script/manage update-povw-minter --broadcast @@ -507,6 +518,9 @@ main() { upgrade-staking) contract_type="staking" ;; + upgrade-supply-calculator) + contract_type="supply-calculator" + ;; *) echo "❌ Unknown upgrade command: $COMMAND" exit 1 @@ -553,6 +567,9 @@ main() { upgrade-staking) forge_script "UpgradeStakingRewards" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; + upgrade-supply-calculator) + forge_script "UpgradeSupplyCalculator" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" + ;; rollback-zkc) forge_script "RollbackZKC" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; @@ -562,6 +579,9 @@ main() { rollback-staking) forge_script "RollbackStakingRewards" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; + rollback-supply-calculator) + forge_script "RollbackSupplyCalculator" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" + ;; update-povw-minter) forge_script "UpdatePOVWMinter" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; From 537c0770d2d0ae3e6fd6330dca548ae0dc936858 Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:03:01 -0500 Subject: [PATCH 03/11] Rename functions --- deployment.toml | 9 +++++---- script/Upgrade.s.sol | 19 ------------------- src/calculators/SupplyCalculator.sol | 14 +++++++++++--- test/calculators/SupplyCalculator.t.sol | 4 ++-- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/deployment.toml b/deployment.toml index 446c847..45348c2 100644 --- a/deployment.toml +++ b/deployment.toml @@ -25,13 +25,14 @@ staking-rewards-impl-prev = "0x2d57bc44e3c1fe4ceec8f2bf9ea0ac954b527e43" staking-rewards-deployer = "0xb13573c6ceb505a7bdd4fa3ad7b473c5c5d36b19" povw-minter = "0xbfce7c2d5e7eddeab71b3eeed770713c8b755397" staking-minter = "0x459d87d54808fac136ddcf439fcc1d8a238311c7" -circulating-zkc = "0x15eB8cA378E33381c867573EF2f25Ca1010f866d" -circulating-zkc-impl = "0xB0615B524a560b307C8fc486928B989a8b1234DA" -circulating-zkc-admin = "0xb13573c6ceb505a7bdd4fa3ad7b473c5c5d36b19" -circulating-zkc-commit = "9d0b6a9b541d3b6980893b3385d33d12b173f232" +supply-calculator = "0x15eB8cA378E33381c867573EF2f25Ca1010f866d" +supply-calculator-impl = "0xb068e8f58ff45dc3dbca8f1f3bfd9c34ee2b3500" +supply-calculator-admin = "0xb13573c6ceb505a7bdd4fa3ad7b473c5c5d36b19" +supply-calculator-commit = "14facdc" zkc-commit = "5b8310b" vezkc-commit = "caa29df" staking-rewards-commit = "24e6b5a" +supply-calculator-admin-2 = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" [deployment.ethereum-sepolia] name = "Ethereum Sepolia" diff --git a/script/Upgrade.s.sol b/script/Upgrade.s.sol index 51ba583..5b5b08e 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -486,25 +486,6 @@ contract UpgradeStakingRewards is BaseDeployment { } } -/** - * Sample Usage for SupplyCalculator upgrade: - * - * # First, create reference build from deployed SupplyCalculator commit: - * export DEPLOYED_COMMIT=$(python3 -c "import tomlkit; print(tomlkit.load(open('deployment.toml'))['deployment']['$CHAIN_KEY']['supply-calculator-commit'])") - * WORKTREE_PATH="../supply-calculator-reference-${DEPLOYED_COMMIT}" - * git worktree add "$WORKTREE_PATH" "$DEPLOYED_COMMIT" - * cd "$WORKTREE_PATH" - * forge build --profile reference - * cp -R out-reference/build-info "$OLDPWD/build-info-reference" - * cd "$OLDPWD" - * - * # Then run upgrade: - * export CHAIN_KEY="anvil" - * forge script script/Upgrade.s.sol:UpgradeSupplyCalculator \ - * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ - * --broadcast \ - * --rpc-url http://127.0.0.1:8545 - */ contract UpgradeSupplyCalculator is BaseDeployment { function run() public { (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); diff --git a/src/calculators/SupplyCalculator.sol b/src/calculators/SupplyCalculator.sol index 6664c7f..76481dc 100644 --- a/src/calculators/SupplyCalculator.sol +++ b/src/calculators/SupplyCalculator.sol @@ -49,6 +49,7 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad /// @notice Calculate the current circulating supply /// @dev Formula: unlocked + (zkc.claimedTotalSupply() - zkc.INITIAL_SUPPLY()) + /// @dev Essentially, unlocked tokens from the initial mint + claimed rewards from PoVW and staking rewards /// @return The current circulating supply of ZKC tokens function circulatingSupply() public view returns (uint256) { uint256 claimedTotal = zkc.claimedTotalSupply(); @@ -76,6 +77,9 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad return _roundToWholeTokens(supply); } + /// @notice Get the total supply + /// @dev This represents the theoretical total supply of ZKC tokens based on the current epoch + /// @return The total supply of ZKC tokens function totalSupply() public view returns (uint256) { return IERC20(address(zkc)).totalSupply(); } @@ -98,7 +102,11 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad return _roundToWholeTokens(supply); } - function totalClaimedSupply() public view returns (uint256) { + /// @notice Get the total claimed supply + /// @dev This represents the initial supply that was minted and allocated to initial minters, + /// as well as tokens that have been claimed (and thus minted) via PoVW or Staking rewards. + /// @return The total amount of tokens that have been claimed + function claimedTotalSupply() public view returns (uint256) { return zkc.claimedTotalSupply(); } @@ -106,7 +114,7 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad /// @dev Returns value in wei (18 decimals) but rounded such that when converted to whole tokens it's rounded /// @dev Uses the actual claimed/minted supply /// @return The claimed total supply rounded to nearest whole token in 18dp format - function totalClaimedSupplyRounded() public view returns (uint256) { + function claimedTotalSupplyRounded() public view returns (uint256) { uint256 supply = zkc.claimedTotalSupply(); return _roundTo18dp(supply); } @@ -115,7 +123,7 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad /// @dev Returns the value in regular representation (divided by 10^18) rounded to nearest whole token /// @dev Uses the actual claimed/minted supply /// @return The claimed total supply as a whole number - function totalClaimedSupplyAmountRounded() public view returns (uint256) { + function claimedTotalSupplyAmountRounded() public view returns (uint256) { uint256 supply = zkc.claimedTotalSupply(); return _roundToWholeTokens(supply); } diff --git a/test/calculators/SupplyCalculator.t.sol b/test/calculators/SupplyCalculator.t.sol index 334f5a9..5d1812d 100644 --- a/test/calculators/SupplyCalculator.t.sol +++ b/test/calculators/SupplyCalculator.t.sol @@ -226,8 +226,8 @@ contract SupplyCalculatorTest is Test { assertEq(claimedSupply, zkc.INITIAL_SUPPLY() + rewards); // Get rounded values - uint256 rounded18dp = supplyCalculator.totalClaimedSupplyRounded(); - uint256 roundedAmount = supplyCalculator.totalClaimedSupplyAmountRounded(); + uint256 rounded18dp = supplyCalculator.claimedTotalSupplyRounded(); + uint256 roundedAmount = supplyCalculator.claimedTotalSupplyAmountRounded(); assertEq(rounded18dp, 1001234568000000000000000000); assertEq(roundedAmount, 1001234568); From f2f022a5fb9fa01d63e5a709f726c01a27bc038c Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:17:02 -0500 Subject: [PATCH 04/11] Fmt + latest deployment --- deployment.toml | 4 ++-- script/Config.s.sol | 9 ++++++--- script/Rollback.s.sol | 7 +++++-- script/Update.s.sol | 6 +++--- script/Upgrade.s.sol | 8 ++++++-- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/deployment.toml b/deployment.toml index 45348c2..2584e23 100644 --- a/deployment.toml +++ b/deployment.toml @@ -26,9 +26,9 @@ staking-rewards-deployer = "0xb13573c6ceb505a7bdd4fa3ad7b473c5c5d36b19" povw-minter = "0xbfce7c2d5e7eddeab71b3eeed770713c8b755397" staking-minter = "0x459d87d54808fac136ddcf439fcc1d8a238311c7" supply-calculator = "0x15eB8cA378E33381c867573EF2f25Ca1010f866d" -supply-calculator-impl = "0xb068e8f58ff45dc3dbca8f1f3bfd9c34ee2b3500" +supply-calculator-impl = "0x1ff6f81ea7f5e6feafaafb0fc5983576096ded1d" supply-calculator-admin = "0xb13573c6ceb505a7bdd4fa3ad7b473c5c5d36b19" -supply-calculator-commit = "14facdc" +supply-calculator-commit = "537c077" zkc-commit = "5b8310b" vezkc-commit = "caa29df" staking-rewards-commit = "24e6b5a" diff --git a/script/Config.s.sol b/script/Config.s.sol index 24b7dd3..1c744f8 100644 --- a/script/Config.s.sol +++ b/script/Config.s.sol @@ -80,9 +80,12 @@ library ConfigLoader { config.stakingMinter = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".staking-minter")); config.supplyCalculator = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator")); config.supplyCalculatorImpl = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-impl")); - config.supplyCalculatorImplPrev = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-impl-prev")); - config.supplyCalculatorAdmin = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-admin")); - config.supplyCalculatorAdmin2 = _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-admin-2")); + config.supplyCalculatorImplPrev = + _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-impl-prev")); + config.supplyCalculatorAdmin = + _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-admin")); + config.supplyCalculatorAdmin2 = + _readAddressOrZero(vm, toml, string.concat(keyPrefix, ".supply-calculator-admin-2")); // Read per-contract deployment commits, default to empty string if not found string memory zkcCommitKey = string.concat(keyPrefix, ".zkc-commit"); diff --git a/script/Rollback.s.sol b/script/Rollback.s.sol index 2d1f002..4316c4d 100644 --- a/script/Rollback.s.sol +++ b/script/Rollback.s.sol @@ -222,7 +222,8 @@ contract RollbackSupplyCalculator is BaseDeployment { (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); require(config.supplyCalculator != address(0), "SupplyCalculator not deployed"); require( - config.supplyCalculatorImplPrev != address(0), "No previous SupplyCalculator implementation found for rollback" + config.supplyCalculatorImplPrev != address(0), + "No previous SupplyCalculator implementation found for rollback" ); vm.startBroadcast(); @@ -254,7 +255,9 @@ contract RollbackSupplyCalculator is BaseDeployment { // Verify rollback SupplyCalculator supplyCalculatorContract = SupplyCalculator(config.supplyCalculator); IAccessControl accessControl = IAccessControl(config.supplyCalculator); - console2.log("Proxy still points to SupplyCalculator: ", address(supplyCalculatorContract) == config.supplyCalculator); + console2.log( + "Proxy still points to SupplyCalculator: ", address(supplyCalculatorContract) == config.supplyCalculator + ); console2.log( "Admin role still assigned: ", accessControl.hasRole(supplyCalculatorContract.ADMIN_ROLE(), config.supplyCalculatorAdmin) diff --git a/script/Update.s.sol b/script/Update.s.sol index 09dd140..9b65505 100644 --- a/script/Update.s.sol +++ b/script/Update.s.sol @@ -1020,9 +1020,9 @@ contract RemoveSupplyCalculatorAdmin is BaseDeployment { // Safety check: Ensure at least one other admin will remain IAccessControl accessControl = IAccessControl(config.supplyCalculator); - address otherAdmin = (config.supplyCalculatorAdmin != address(0) && config.supplyCalculatorAdmin != adminToRemove) - ? config.supplyCalculatorAdmin - : config.supplyCalculatorAdmin2; + address otherAdmin = ( + config.supplyCalculatorAdmin != address(0) && config.supplyCalculatorAdmin != adminToRemove + ) ? config.supplyCalculatorAdmin : config.supplyCalculatorAdmin2; require( otherAdmin != adminToRemove && otherAdmin != address(0) && accessControl.hasRole(supplyCalculatorContract.ADMIN_ROLE(), otherAdmin), diff --git a/script/Upgrade.s.sol b/script/Upgrade.s.sol index 5b5b08e..1eac062 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -505,7 +505,9 @@ contract UpgradeSupplyCalculator is BaseDeployment { } else { // Get the SupplyCalculator commit hash from deployment config for commit-specific reference build string memory supplyCalculatorCommit = config.supplyCalculatorCommit; - require(bytes(supplyCalculatorCommit).length > 0, "SupplyCalculator commit hash not found in deployment config"); + require( + bytes(supplyCalculatorCommit).length > 0, "SupplyCalculator commit hash not found in deployment config" + ); string memory referenceBuildDir = string.concat("build-info-reference-", supplyCalculatorCommit); opts.referenceContract = string.concat(referenceBuildDir, ":SupplyCalculator"); @@ -537,7 +539,9 @@ contract UpgradeSupplyCalculator is BaseDeployment { // Verify upgrade SupplyCalculator supplyCalculatorContract = SupplyCalculator(config.supplyCalculator); - console2.log("Proxy still points to SupplyCalculator: ", address(supplyCalculatorContract) == config.supplyCalculator); + console2.log( + "Proxy still points to SupplyCalculator: ", address(supplyCalculatorContract) == config.supplyCalculator + ); console2.log("Implementation updated: ", newImpl != config.supplyCalculatorImpl); console2.log("ZKC token still configured: ", address(supplyCalculatorContract.zkc()) == config.zkc); console2.log("================================================"); From 7a28f5d67530d53935921571ee743e710be67cb4 Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:18:49 -0600 Subject: [PATCH 05/11] Lint fix --- foundry.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/foundry.toml b/foundry.toml index a48a131..31f3189 100644 --- a/foundry.toml +++ b/foundry.toml @@ -40,6 +40,9 @@ fs_permissions = [ { access = "read", path = "deployment_secrets.toml" } ] +[fmt] +line_length = 120 + # Invariant testing configuration [invariant] runs = 50 # Faster execution while maintaining good coverage From 0069ea4ace3d8c4bd1792f322a94bfe76d48ffe2 Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:24:36 -0600 Subject: [PATCH 06/11] Fmt --- foundry.toml | 3 --- script/Rollback.s.sol | 10 ++++------ script/Update.s.sol | 14 ++++++++------ src/ZKC.sol | 10 ++++++---- src/libraries/Checkpoints.sol | 4 +--- src/veZKC.sol | 10 +--------- test/StakingRewards.t.sol | 7 ++++--- test/calculators/SupplyCalculator.t.sol | 5 +++-- test/invariant/veZKCHandler.sol | 6 ++---- 9 files changed, 29 insertions(+), 40 deletions(-) diff --git a/foundry.toml b/foundry.toml index 31f3189..a48a131 100644 --- a/foundry.toml +++ b/foundry.toml @@ -40,9 +40,6 @@ fs_permissions = [ { access = "read", path = "deployment_secrets.toml" } ] -[fmt] -line_length = 120 - # Invariant testing configuration [invariant] runs = 50 # Faster execution while maintaining good coverage diff --git a/script/Rollback.s.sol b/script/Rollback.s.sol index 4316c4d..d875a3b 100644 --- a/script/Rollback.s.sol +++ b/script/Rollback.s.sol @@ -176,9 +176,8 @@ contract RollbackStakingRewards is BaseDeployment { require(_getCodeSize(config.stakingRewardsImplPrev) > 0, "Previous implementation has no code"); // Perform rollback by directly upgrading to previous implementation (unsafe) - (bool success,) = config.stakingRewards.call( - abi.encodeWithSignature("upgradeToAndCall(address,bytes)", config.stakingRewardsImplPrev, "") - ); + (bool success,) = config.stakingRewards + .call(abi.encodeWithSignature("upgradeToAndCall(address,bytes)", config.stakingRewardsImplPrev, "")); require(success, "Failed to rollback StakingRewards implementation"); address rolledBackImpl = _getImplementationAddress(config.stakingRewards); @@ -237,9 +236,8 @@ contract RollbackSupplyCalculator is BaseDeployment { require(_getCodeSize(config.supplyCalculatorImplPrev) > 0, "Previous implementation has no code"); // Perform rollback by directly upgrading to previous implementation (unsafe) - (bool success,) = config.supplyCalculator.call( - abi.encodeWithSignature("upgradeToAndCall(address,bytes)", config.supplyCalculatorImplPrev, "") - ); + (bool success,) = config.supplyCalculator + .call(abi.encodeWithSignature("upgradeToAndCall(address,bytes)", config.supplyCalculatorImplPrev, "")); require(success, "Failed to rollback SupplyCalculator implementation"); address rolledBackImpl = _getImplementationAddress(config.supplyCalculator); diff --git a/script/Update.s.sol b/script/Update.s.sol index 9b65505..b240931 100644 --- a/script/Update.s.sol +++ b/script/Update.s.sol @@ -1020,9 +1020,10 @@ contract RemoveSupplyCalculatorAdmin is BaseDeployment { // Safety check: Ensure at least one other admin will remain IAccessControl accessControl = IAccessControl(config.supplyCalculator); - address otherAdmin = ( - config.supplyCalculatorAdmin != address(0) && config.supplyCalculatorAdmin != adminToRemove - ) ? config.supplyCalculatorAdmin : config.supplyCalculatorAdmin2; + address otherAdmin = (config.supplyCalculatorAdmin != address(0) + && config.supplyCalculatorAdmin != adminToRemove) + ? config.supplyCalculatorAdmin + : config.supplyCalculatorAdmin2; require( otherAdmin != adminToRemove && otherAdmin != address(0) && accessControl.hasRole(supplyCalculatorContract.ADMIN_ROLE(), otherAdmin), @@ -1269,9 +1270,10 @@ contract RemoveAdminAll is BaseDeployment { ); // Check StakingRewards contract - address stakingOtherAdmin = ( - config.stakingRewardsAdmin != address(0) && config.stakingRewardsAdmin != adminToRemove - ) ? config.stakingRewardsAdmin : config.stakingRewardsAdmin2; + address stakingOtherAdmin = (config.stakingRewardsAdmin != address(0) + && config.stakingRewardsAdmin != adminToRemove) + ? config.stakingRewardsAdmin + : config.stakingRewardsAdmin2; assert(stakingOtherAdmin != adminToRemove && stakingOtherAdmin != address(0)); require( stakingOtherAdmin != address(0) diff --git a/src/ZKC.sol b/src/ZKC.sol index a1c39fa..4442121 100644 --- a/src/ZKC.sol +++ b/src/ZKC.sol @@ -2,11 +2,13 @@ pragma solidity 0.8.26; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import {ERC20BurnableUpgradeable} from - "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import { + ERC20BurnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; -import {ERC20PermitUpgradeable} from - "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import { + ERC20PermitUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; diff --git a/src/libraries/Checkpoints.sol b/src/libraries/Checkpoints.sol index 168f0aa..176fd28 100644 --- a/src/libraries/Checkpoints.sol +++ b/src/libraries/Checkpoints.sol @@ -139,9 +139,7 @@ library Checkpoints { require(newRewardAmount >= 0, "Checkpoints: reward amount underflow"); Point memory newUserPoint = Point({ - votingAmount: uint256(newVotingAmount), - rewardAmount: uint256(newRewardAmount), - updatedAt: block.timestamp + votingAmount: uint256(newVotingAmount), rewardAmount: uint256(newRewardAmount), updatedAt: block.timestamp }); // Update user checkpoint diff --git a/src/veZKC.sol b/src/veZKC.sol index f24f6e3..e9b85f8 100644 --- a/src/veZKC.sol +++ b/src/veZKC.sol @@ -20,15 +20,7 @@ import {ZKC} from "./ZKC.sol"; /// @title veZKC - Vote Escrowed ZK Coin /// @notice Staking contracts for ZKC, granting voting and reward power. -contract veZKC is - Initializable, - AccessControlUpgradeable, - UUPSUpgradeable, - EIP712Upgradeable, - Votes, - Rewards, - Staking -{ +contract veZKC is Initializable, AccessControlUpgradeable, UUPSUpgradeable, EIP712Upgradeable, Votes, Rewards, Staking { bytes32 public constant ADMIN_ROLE = DEFAULT_ADMIN_ROLE; /// @notice Returns the ZKC token address diff --git a/test/StakingRewards.t.sol b/test/StakingRewards.t.sol index d5d2e91..d85ef6c 100644 --- a/test/StakingRewards.t.sol +++ b/test/StakingRewards.t.sol @@ -263,8 +263,8 @@ contract StakingRewardsTest is Test { epochs[0] = 2; epochs[1] = 0; epochs[2] = 1; - uint256 expected = - zkc.getStakingEmissionsForEpoch(0) + zkc.getStakingEmissionsForEpoch(1) + zkc.getStakingEmissionsForEpoch(2); + uint256 expected = zkc.getStakingEmissionsForEpoch(0) + zkc.getStakingEmissionsForEpoch(1) + + zkc.getStakingEmissionsForEpoch(2); uint256[] memory calc = rewards.calculateRewards(user1, epochs); uint256 sum; for (uint256 i; i < calc.length; ++i) { @@ -435,7 +435,8 @@ contract StakingRewardsTest is Test { ); assertEq( selfClaimed + recipientClaimed, - zkc.getStakingEmissionsForEpoch(0) + zkc.getStakingEmissionsForEpoch(1) + zkc.getStakingEmissionsForEpoch(2), + zkc.getStakingEmissionsForEpoch(0) + zkc.getStakingEmissionsForEpoch(1) + + zkc.getStakingEmissionsForEpoch(2), "Total claimed should equal all emissions" ); } diff --git a/test/calculators/SupplyCalculator.t.sol b/test/calculators/SupplyCalculator.t.sol index 5d1812d..5322170 100644 --- a/test/calculators/SupplyCalculator.t.sol +++ b/test/calculators/SupplyCalculator.t.sol @@ -32,8 +32,9 @@ contract SupplyCalculatorTest is Test { ZKC implementation = new ZKC(); // Deploy proxy and initialize - bytes memory initData = - abi.encodeWithSelector(ZKC.initialize.selector, minter1, minter2, implementation.INITIAL_SUPPLY(), 0, owner); + bytes memory initData = abi.encodeWithSelector( + ZKC.initialize.selector, minter1, minter2, implementation.INITIAL_SUPPLY(), 0, owner + ); ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); zkc = ZKC(address(proxy)); diff --git a/test/invariant/veZKCHandler.sol b/test/invariant/veZKCHandler.sol index 236894f..b0f0f03 100644 --- a/test/invariant/veZKCHandler.sol +++ b/test/invariant/veZKCHandler.sol @@ -308,10 +308,8 @@ contract veZKCHandler is Test { // Only record if this is a new timestamp and it's not block 0 if ( timestamp > 0 - && ( - ghost_historicalTimestamps.length == 0 - || ghost_historicalTimestamps[ghost_historicalTimestamps.length - 1] < timestamp - ) + && (ghost_historicalTimestamps.length == 0 + || ghost_historicalTimestamps[ghost_historicalTimestamps.length - 1] < timestamp) ) { ghost_historicalTimestamps.push(timestamp); From ec4d6b369ebd415ad217ff48e4c80687d854402b Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:12:10 -0600 Subject: [PATCH 07/11] Refactor formula --- script/Update.s.sol | 50 +++--- src/circulating/CirculatingZKC.sol | 93 ++++++++++ test/.DS_Store | Bin 0 -> 6148 bytes test/circulating/CirculatingZKC.t.sol | 241 ++++++++++++++++++++++++++ 4 files changed, 364 insertions(+), 20 deletions(-) create mode 100644 src/circulating/CirculatingZKC.sol create mode 100644 test/.DS_Store create mode 100644 test/circulating/CirculatingZKC.t.sol diff --git a/script/Update.s.sol b/script/Update.s.sol index b240931..b5c33df 100644 --- a/script/Update.s.sol +++ b/script/Update.s.sol @@ -9,6 +9,7 @@ import {ZKC} from "../src/ZKC.sol"; import {SupplyCalculator} from "../src/calculators/SupplyCalculator.sol"; import {veZKC} from "../src/veZKC.sol"; import {StakingRewards} from "../src/rewards/StakingRewards.sol"; +import {Supply} from "../src/libraries/Supply.sol"; /** * Sample Usage for setting POVW minter role: @@ -201,7 +202,7 @@ contract UpdateStakingMinter is BaseDeployment { * * # Direct execution: * export CHAIN_KEY="anvil" - * export NEW_UNLOCKED="750000000000000000000000000" # 750M tokens + * export NEW_LOCKED="250000000000000000000000000" # 250M tokens * * forge script script/Update.s.sol:UpdateSupplyCalculatorUnlocked \ * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ @@ -210,7 +211,7 @@ contract UpdateStakingMinter is BaseDeployment { * * # Gnosis Safe execution (print call data only): * export CHAIN_KEY="anvil" - * export NEW_UNLOCKED="750000000000000000000000000" # 750M tokens + * export NEW_LOCKED="250000000000000000000000000" # 250M tokens * export GNOSIS_EXECUTE=true * * forge script script/Update.s.sol:UpdateSupplyCalculatorUnlocked \ @@ -224,9 +225,9 @@ contract UpdateSupplyCalculatorUnlocked is BaseDeployment { (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); require(config.supplyCalculator != address(0), "SupplyCalculator address not set in deployment.toml"); - // Get new unlocked value from environment - uint256 newUnlocked = vm.envUint("NEW_UNLOCKED"); - require(newUnlocked > 0, "NEW_UNLOCKED must be greater than 0"); + // Get new locked value from environment + uint256 newLocked = vm.envUint("NEW_LOCKED"); + require(newLocked > 0, "NEW_LOCKED must be greater than 0"); // Check for Gnosis Safe execution mode bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); @@ -236,9 +237,12 @@ contract UpdateSupplyCalculatorUnlocked is BaseDeployment { // Get current values for logging uint256 currentUnlocked = supplyCalculator.unlocked(); + uint256 currentLocked = supplyCalculator.locked(); uint256 currentCirculatingSupply = supplyCalculator.circulatingSupply(); console2.log("================================================"); + console2.log("Current locked amount: ", currentLocked); + console2.log("Current locked amount (in tokens): ", currentLocked / 10 ** 18); console2.log("Current unlocked amount: ", currentUnlocked); console2.log("Current unlocked amount (in tokens): ", currentUnlocked / 10 ** 18); console2.log("Current circulating supply: ", currentCirculatingSupply); @@ -249,12 +253,15 @@ contract UpdateSupplyCalculatorUnlocked is BaseDeployment { // Print Gnosis Safe transaction info for manual execution _printGnosisSafeInfo(config.supplyCalculator, newUnlocked); - // Calculate expected new circulating supply for display - uint256 expectedNewCirculatingSupply = currentCirculatingSupply - currentUnlocked + newUnlocked; + // Calculate expected new values for display + uint256 expectedNewUnlocked = Supply.INITIAL_SUPPLY - newLocked; + uint256 expectedNewCirculatingSupply = currentCirculatingSupply - currentUnlocked + expectedNewUnlocked; console2.log("Expected after update:"); - console2.log("New unlocked amount: ", newUnlocked); - console2.log("New unlocked amount (in tokens): ", newUnlocked / 10 ** 18); + console2.log("New locked amount: ", newLocked); + console2.log("New locked amount (in tokens): ", newLocked / 10 ** 18); + console2.log("New unlocked amount: ", expectedNewUnlocked); + console2.log("New unlocked amount (in tokens): ", expectedNewUnlocked / 10 ** 18); console2.log("New circulating supply: ", expectedNewCirculatingSupply); console2.log("New circulating supply (in tokens): ", expectedNewCirculatingSupply / 10 ** 18); console2.log("================================================"); @@ -266,22 +273,25 @@ contract UpdateSupplyCalculatorUnlocked is BaseDeployment { // Get updated values uint256 updatedUnlocked = supplyCalculator.unlocked(); + uint256 updatedLocked = supplyCalculator.locked(); uint256 updatedCirculatingSupply = supplyCalculator.circulatingSupply(); vm.stopBroadcast(); - console2.log("Updated unlocked value!"); + console2.log("Updated locked value!"); console2.log("================================================"); + console2.log("New locked amount: ", updatedLocked); + console2.log("New locked amount (in tokens): ", updatedLocked / 10 ** 18); console2.log("New unlocked amount: ", updatedUnlocked); console2.log("New unlocked amount (in tokens): ", updatedUnlocked / 10 ** 18); console2.log("New circulating supply: ", updatedCirculatingSupply); console2.log("New circulating supply (in tokens): ", updatedCirculatingSupply / 10 ** 18); console2.log("================================================"); - console2.log("Change in unlocked: ", newUnlocked > currentUnlocked ? "+" : "-"); - if (newUnlocked > currentUnlocked) { - console2.log(" Amount increased: ", (newUnlocked - currentUnlocked) / 10 ** 18, " tokens"); + console2.log("Change in locked: ", newLocked > currentLocked ? "+" : "-"); + if (newLocked > currentLocked) { + console2.log(" Amount increased: ", (newLocked - currentLocked) / 10 ** 18, " tokens"); } else { - console2.log(" Amount decreased: ", (currentUnlocked - newUnlocked) / 10 ** 18, " tokens"); + console2.log(" Amount decreased: ", (currentLocked - newLocked) / 10 ** 18, " tokens"); } } } @@ -294,18 +304,18 @@ contract UpdateSupplyCalculatorUnlocked is BaseDeployment { console2.log("================================"); console2.log("=== GNOSIS SAFE UPDATE INFO ==="); console2.log("Target Address (To): ", targetAddress); - console2.log("Function: updateUnlockedValue(uint256)"); - console2.log("New Unlocked Value: ", newUnlocked); - console2.log("New Unlocked Value (in tokens): ", newUnlocked / 10 ** 18); + console2.log("Function: updateLockedValue(uint256)"); + console2.log("New Locked Value: ", newLocked); + console2.log("New Locked Value (in tokens): ", newLocked / 10 ** 18); - bytes memory callData = abi.encodeWithSignature("updateUnlockedValue(uint256)", newUnlocked); + bytes memory callData = abi.encodeWithSignature("updateLockedValue(uint256)", newLocked); console2.log(""); console2.log("Calldata:"); console2.logBytes(callData); console2.log(""); console2.log("Expected Events on Successful Execution:"); - console2.log("1. UnlockedValueUpdated(uint256 oldValue, uint256 newValue)"); - console2.log(" - newValue: ", newUnlocked); + console2.log("1. LockedValueUpdated(uint256 oldValue, uint256 newValue)"); + console2.log(" - newValue: ", newLocked); console2.log("================================"); } } diff --git a/src/circulating/CirculatingZKC.sol b/src/circulating/CirculatingZKC.sol new file mode 100644 index 0000000..e4d13f2 --- /dev/null +++ b/src/circulating/CirculatingZKC.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IZKC} from "../interfaces/IZKC.sol"; +import {Supply} from "../libraries/Supply.sol"; + +/// @title CirculatingZKC - Circulating Supply Calculator for ZKC +/// @notice Contract to track and calculate the circulating supply of ZKC tokens +/// @dev Uses an admin-controlled unlocked value plus claimed tokens to calculate circulating supply +contract CirculatingZKC is Initializable, AccessControlUpgradeable, UUPSUpgradeable { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + /// @notice Admin role identifier + bytes32 public immutable ADMIN_ROLE = DEFAULT_ADMIN_ROLE; + + /// @notice Reference to the ZKC token contract + IZKC public zkc; + + /// @notice Amount of tokens considered unlocked and in circulation + uint256 public unlocked; + + /// @notice Amount of tokens considered locked and not in circulation + uint256 public locked; + + /// @notice Emitted when the unlocked value is updated + /// @param oldValue The previous unlocked value + /// @param newValue The new unlocked value + event UnlockedValueUpdated(uint256 oldValue, uint256 newValue); + + /// @notice Emitted when the locked value is updated + /// @param oldValue The previous locked value + /// @param newValue The new locked value + event LockedValueUpdated(uint256 oldValue, uint256 newValue); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the CirculatingZKC contract + /// @param _zkc Address of the ZKC token contract + /// @param _initialUnlocked Initial value for the unlocked tokens + /// @param _admin Address that will be granted the admin role + function initialize(address _zkc, uint256 _initialUnlocked, address _admin) public initializer { + require(_zkc != address(0), "ZKC address cannot be zero"); + require(_admin != address(0), "Admin address cannot be zero"); + + __AccessControl_init(); + __UUPSUpgradeable_init(); + + zkc = IZKC(_zkc); + unlocked = _initialUnlocked; + locked = Supply.INITIAL_SUPPLY - _initialUnlocked; + _grantRole(ADMIN_ROLE, _admin); + } + + /// @notice Calculate the current circulating supply + /// @dev Formula: claimedTotalSupply - locked + /// @return The current circulating supply of ZKC tokens + function circulatingSupply() public view returns (uint256) { + uint256 claimedTotal = zkc.claimedTotalSupply(); + return claimedTotal - locked; + } + + /// @notice Update the unlocked value + /// @dev Only callable by accounts with ADMIN_ROLE + /// @dev Also updates locked to keep them in sync: locked = INITIAL_SUPPLY - unlocked + /// @param _newUnlocked The new value for unlocked tokens + function updateUnlockedValue(uint256 _newUnlocked) external onlyRole(ADMIN_ROLE) { + uint256 oldValue = unlocked; + unlocked = _newUnlocked; + locked = Supply.INITIAL_SUPPLY - _newUnlocked; + emit UnlockedValueUpdated(oldValue, _newUnlocked); + } + + /// @notice Update the locked value + /// @dev Only callable by accounts with ADMIN_ROLE + /// @dev Also updates unlocked to keep them in sync: unlocked = INITIAL_SUPPLY - locked + /// @param _newLocked The new value for locked tokens + function updateLockedValue(uint256 _newLocked) external onlyRole(ADMIN_ROLE) { + uint256 oldValue = locked; + locked = _newLocked; + unlocked = Supply.INITIAL_SUPPLY - _newLocked; + emit LockedValueUpdated(oldValue, _newLocked); + } + + /// @notice Authorize contract upgrades (UUPS pattern) + /// @dev Only accounts with ADMIN_ROLE can authorize upgrades + /// @param newImplementation Address of the new implementation contract + function _authorizeUpgrade(address newImplementation) internal override onlyRole(ADMIN_ROLE) {} +} diff --git a/test/.DS_Store b/test/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..307b4c4a06cc486e6f7f351f0af1d87839b06631 GIT binary patch literal 6148 zcmeHLy-or_5S|4Jei9`%T3$tEL#2($Nl+hv(uUt45jaG!AQtxs`U1WHu`#yB#snXS z!o=2Zb~mu=9T*!mW+&PC_I77>K0b20EJP$q<7Sa4Pef@n##9zA#(17v#1eXB2Potk zW9m|kM%1BxB-#x)1)KtZO#yy)bJV~aJA^mi-*&fMtTck&KvCY_S{>O7%H?`Ds3Ri% zS{S`Op3Dx{MK`|@JvcL2cvzOQU>Z=52Jn4qp^fO$Vw)(uuZlCdZWLZ_r+boX?3%q` z$@HsOi)W(BJd@PMZfG;(@x_eEW#{^IKG~IAbe>r~I&0Nat7hcBdxe@q^lGe&r3$r= z5f~F?@~Av*PVbc_v5`wf)<@d>LS~r3>CmA%*eQms&)UrPp_;1pOW!25%T z#^`Gd70RsxPF?|kDGWifT)Tt%}CRaiKz)0;i8-+2Ny@|5uQPIgdL)Ut_2cBQW Date: Thu, 20 Nov 2025 14:52:04 -0600 Subject: [PATCH 08/11] Script fixes and simplify supply calc --- .vscode/settings.json | 2 +- deployment.toml | 3 +- script/Update.s.sol | 20 +- script/Upgrade.s.sol | 2 + src/calculators/SupplyCalculator.sol | 32 +++- src/circulating/CirculatingZKC.sol | 93 --------- test/calculators/SupplyCalculator.t.sol | 82 +++++++- test/circulating/CirculatingZKC.t.sol | 241 ------------------------ 8 files changed, 118 insertions(+), 357 deletions(-) delete mode 100644 src/circulating/CirculatingZKC.sol delete mode 100644 test/circulating/CirculatingZKC.t.sol diff --git a/.vscode/settings.json b/.vscode/settings.json index 4276b4b..b2a9e6f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "solidity.packageDefaultDependenciesContractsDirectory": "src", "solidity.packageDefaultDependenciesDirectory": "lib", - "solidity.monoRepoSupport": false + "solidity.monoRepoSupport": false, } \ No newline at end of file diff --git a/deployment.toml b/deployment.toml index 2584e23..3ebfc65 100644 --- a/deployment.toml +++ b/deployment.toml @@ -28,11 +28,12 @@ staking-minter = "0x459d87d54808fac136ddcf439fcc1d8a238311c7" supply-calculator = "0x15eB8cA378E33381c867573EF2f25Ca1010f866d" supply-calculator-impl = "0x1ff6f81ea7f5e6feafaafb0fc5983576096ded1d" supply-calculator-admin = "0xb13573c6ceb505a7bdd4fa3ad7b473c5c5d36b19" +supply-calculator-admin-2 = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" supply-calculator-commit = "537c077" zkc-commit = "5b8310b" vezkc-commit = "caa29df" staking-rewards-commit = "24e6b5a" -supply-calculator-admin-2 = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" + [deployment.ethereum-sepolia] name = "Ethereum Sepolia" diff --git a/script/Update.s.sol b/script/Update.s.sol index b5c33df..c2a98f9 100644 --- a/script/Update.s.sol +++ b/script/Update.s.sol @@ -249,27 +249,29 @@ contract UpdateSupplyCalculatorUnlocked is BaseDeployment { console2.log("Current circulating supply (in tokens): ", currentCirculatingSupply / 10 ** 18); console2.log("================================================"); + // Calculate new unlocked value from new locked value + uint256 newUnlocked = Supply.INITIAL_SUPPLY - newLocked; + if (gnosisExecute) { // Print Gnosis Safe transaction info for manual execution - _printGnosisSafeInfo(config.supplyCalculator, newUnlocked); + _printGnosisSafeInfo(config.supplyCalculator, newLocked); // Calculate expected new values for display - uint256 expectedNewUnlocked = Supply.INITIAL_SUPPLY - newLocked; - uint256 expectedNewCirculatingSupply = currentCirculatingSupply - currentUnlocked + expectedNewUnlocked; + uint256 expectedNewCirculatingSupply = currentCirculatingSupply - currentUnlocked + newUnlocked; console2.log("Expected after update:"); console2.log("New locked amount: ", newLocked); console2.log("New locked amount (in tokens): ", newLocked / 10 ** 18); - console2.log("New unlocked amount: ", expectedNewUnlocked); - console2.log("New unlocked amount (in tokens): ", expectedNewUnlocked / 10 ** 18); + console2.log("New unlocked amount: ", newUnlocked); + console2.log("New unlocked amount (in tokens): ", newUnlocked / 10 ** 18); console2.log("New circulating supply: ", expectedNewCirculatingSupply); console2.log("New circulating supply (in tokens): ", expectedNewCirculatingSupply / 10 ** 18); console2.log("================================================"); } else { vm.startBroadcast(); - // Update the unlocked value - supplyCalculator.updateUnlockedValue(newUnlocked); + // Update the locked value (which will also update unlocked) + supplyCalculator.updateLockedValue(newLocked); // Get updated values uint256 updatedUnlocked = supplyCalculator.unlocked(); @@ -298,8 +300,8 @@ contract UpdateSupplyCalculatorUnlocked is BaseDeployment { /// @notice Print Gnosis Safe transaction information for manual updates /// @param targetAddress The SupplyCalculator contract address (target for Gnosis Safe) - /// @param newUnlocked The new unlocked value to set - function _printGnosisSafeInfo(address targetAddress, uint256 newUnlocked) internal pure { + /// @param newLocked The new locked value to set + function _printGnosisSafeInfo(address targetAddress, uint256 newLocked) internal pure { console2.log("================================"); console2.log("================================"); console2.log("=== GNOSIS SAFE UPDATE INFO ==="); diff --git a/script/Upgrade.s.sol b/script/Upgrade.s.sol index 1eac062..b1ea7c2 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -544,6 +544,8 @@ contract UpgradeSupplyCalculator is BaseDeployment { ); console2.log("Implementation updated: ", newImpl != config.supplyCalculatorImpl); console2.log("ZKC token still configured: ", address(supplyCalculatorContract.zkc()) == config.zkc); + console2.log("Circulating supply: ", supplyCalculatorContract.circulatingSupply()); + console2.log("Circulating supply (in tokens): ", supplyCalculatorContract.circulatingSupply() / 10 ** 18); console2.log("================================================"); console2.log("SupplyCalculator Upgrade Complete"); console2.log("New Implementation: ", newImpl); diff --git a/src/calculators/SupplyCalculator.sol b/src/calculators/SupplyCalculator.sol index 76481dc..cc2c283 100644 --- a/src/calculators/SupplyCalculator.sol +++ b/src/calculators/SupplyCalculator.sol @@ -21,11 +21,19 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad /// @notice Amount of tokens considered unlocked and in circulation uint256 public unlocked; + /// @notice Amount of tokens considered locked and not in circulation + uint256 public locked; + /// @notice Emitted when the unlocked value is updated /// @param oldValue The previous unlocked value /// @param newValue The new unlocked value event UnlockedValueUpdated(uint256 oldValue, uint256 newValue); + /// @notice Emitted when the locked value is updated + /// @param oldValue The previous locked value + /// @param newValue The new locked value + event LockedValueUpdated(uint256 oldValue, uint256 newValue); + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -44,21 +52,16 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad zkc = IZKC(_zkc); unlocked = _initialUnlocked; + locked = Supply.INITIAL_SUPPLY - _initialUnlocked; _grantRole(ADMIN_ROLE, _admin); } /// @notice Calculate the current circulating supply - /// @dev Formula: unlocked + (zkc.claimedTotalSupply() - zkc.INITIAL_SUPPLY()) - /// @dev Essentially, unlocked tokens from the initial mint + claimed rewards from PoVW and staking rewards + /// @dev Formula: claimedTotalSupply - locked /// @return The current circulating supply of ZKC tokens function circulatingSupply() public view returns (uint256) { uint256 claimedTotal = zkc.claimedTotalSupply(); - uint256 initialSupply = Supply.INITIAL_SUPPLY; - - // Calculate the claimed rewards from PoVW and staking rewards - uint256 claimedRewards = claimedTotal - initialSupply; - - return unlocked + claimedRewards; + return claimedTotal - locked; } /// @notice Calculate the current circulating supply rounded to the nearest whole token (18dp representation) @@ -160,13 +163,26 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad /// @notice Update the unlocked value /// @dev Only callable by accounts with ADMIN_ROLE + /// @dev Also updates locked to keep them in sync: locked = INITIAL_SUPPLY - unlocked /// @param _newUnlocked The new value for unlocked tokens function updateUnlockedValue(uint256 _newUnlocked) external onlyRole(ADMIN_ROLE) { uint256 oldValue = unlocked; unlocked = _newUnlocked; + locked = Supply.INITIAL_SUPPLY - _newUnlocked; emit UnlockedValueUpdated(oldValue, _newUnlocked); } + /// @notice Update the locked value + /// @dev Only callable by accounts with ADMIN_ROLE + /// @dev Also updates unlocked to keep them in sync: unlocked = INITIAL_SUPPLY - locked + /// @param _newLocked The new value for locked tokens + function updateLockedValue(uint256 _newLocked) external onlyRole(ADMIN_ROLE) { + uint256 oldValue = locked; + locked = _newLocked; + unlocked = Supply.INITIAL_SUPPLY - _newLocked; + emit LockedValueUpdated(oldValue, _newLocked); + } + /// @notice Authorize contract upgrades (UUPS pattern) /// @dev Only accounts with ADMIN_ROLE can authorize upgrades /// @param newImplementation Address of the new implementation contract diff --git a/src/circulating/CirculatingZKC.sol b/src/circulating/CirculatingZKC.sol deleted file mode 100644 index e4d13f2..0000000 --- a/src/circulating/CirculatingZKC.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {IZKC} from "../interfaces/IZKC.sol"; -import {Supply} from "../libraries/Supply.sol"; - -/// @title CirculatingZKC - Circulating Supply Calculator for ZKC -/// @notice Contract to track and calculate the circulating supply of ZKC tokens -/// @dev Uses an admin-controlled unlocked value plus claimed tokens to calculate circulating supply -contract CirculatingZKC is Initializable, AccessControlUpgradeable, UUPSUpgradeable { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - /// @notice Admin role identifier - bytes32 public immutable ADMIN_ROLE = DEFAULT_ADMIN_ROLE; - - /// @notice Reference to the ZKC token contract - IZKC public zkc; - - /// @notice Amount of tokens considered unlocked and in circulation - uint256 public unlocked; - - /// @notice Amount of tokens considered locked and not in circulation - uint256 public locked; - - /// @notice Emitted when the unlocked value is updated - /// @param oldValue The previous unlocked value - /// @param newValue The new unlocked value - event UnlockedValueUpdated(uint256 oldValue, uint256 newValue); - - /// @notice Emitted when the locked value is updated - /// @param oldValue The previous locked value - /// @param newValue The new locked value - event LockedValueUpdated(uint256 oldValue, uint256 newValue); - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialize the CirculatingZKC contract - /// @param _zkc Address of the ZKC token contract - /// @param _initialUnlocked Initial value for the unlocked tokens - /// @param _admin Address that will be granted the admin role - function initialize(address _zkc, uint256 _initialUnlocked, address _admin) public initializer { - require(_zkc != address(0), "ZKC address cannot be zero"); - require(_admin != address(0), "Admin address cannot be zero"); - - __AccessControl_init(); - __UUPSUpgradeable_init(); - - zkc = IZKC(_zkc); - unlocked = _initialUnlocked; - locked = Supply.INITIAL_SUPPLY - _initialUnlocked; - _grantRole(ADMIN_ROLE, _admin); - } - - /// @notice Calculate the current circulating supply - /// @dev Formula: claimedTotalSupply - locked - /// @return The current circulating supply of ZKC tokens - function circulatingSupply() public view returns (uint256) { - uint256 claimedTotal = zkc.claimedTotalSupply(); - return claimedTotal - locked; - } - - /// @notice Update the unlocked value - /// @dev Only callable by accounts with ADMIN_ROLE - /// @dev Also updates locked to keep them in sync: locked = INITIAL_SUPPLY - unlocked - /// @param _newUnlocked The new value for unlocked tokens - function updateUnlockedValue(uint256 _newUnlocked) external onlyRole(ADMIN_ROLE) { - uint256 oldValue = unlocked; - unlocked = _newUnlocked; - locked = Supply.INITIAL_SUPPLY - _newUnlocked; - emit UnlockedValueUpdated(oldValue, _newUnlocked); - } - - /// @notice Update the locked value - /// @dev Only callable by accounts with ADMIN_ROLE - /// @dev Also updates unlocked to keep them in sync: unlocked = INITIAL_SUPPLY - locked - /// @param _newLocked The new value for locked tokens - function updateLockedValue(uint256 _newLocked) external onlyRole(ADMIN_ROLE) { - uint256 oldValue = locked; - locked = _newLocked; - unlocked = Supply.INITIAL_SUPPLY - _newLocked; - emit LockedValueUpdated(oldValue, _newLocked); - } - - /// @notice Authorize contract upgrades (UUPS pattern) - /// @dev Only accounts with ADMIN_ROLE can authorize upgrades - /// @param newImplementation Address of the new implementation contract - function _authorizeUpgrade(address newImplementation) internal override onlyRole(ADMIN_ROLE) {} -} diff --git a/test/calculators/SupplyCalculator.t.sol b/test/calculators/SupplyCalculator.t.sol index 5322170..5bb0552 100644 --- a/test/calculators/SupplyCalculator.t.sol +++ b/test/calculators/SupplyCalculator.t.sol @@ -76,12 +76,17 @@ contract SupplyCalculatorTest is Test { function testInitialization() public view { assertEq(address(supplyCalculator.zkc()), address(zkc)); assertEq(supplyCalculator.unlocked(), INITIAL_UNLOCKED); + assertEq(supplyCalculator.locked(), zkc.INITIAL_SUPPLY() - INITIAL_UNLOCKED); assertTrue(supplyCalculator.hasRole(supplyCalculator.ADMIN_ROLE(), owner)); } function testCirculatingSupplyAfterInitialMint() public { - // Circulating supply should be just unlocked since total minted (1B) + // Circulating supply = claimedTotalSupply - locked + // claimedTotalSupply = INITIAL_SUPPLY (1B), locked = INITIAL_SUPPLY - INITIAL_UNLOCKED + uint256 claimedTotal = zkc.claimedTotalSupply(); + uint256 expectedCirculating = claimedTotal - supplyCalculator.locked(); uint256 circulatingSupply = supplyCalculator.circulatingSupply(); + assertEq(circulatingSupply, expectedCirculating); assertEq(circulatingSupply, INITIAL_UNLOCKED); } @@ -99,13 +104,18 @@ contract SupplyCalculatorTest is Test { vm.prank(stakingMinter); zkc.mintStakingRewardsForRecipient(user, stakingRewards); - uint256 expectedCirculating = INITIAL_UNLOCKED + povwRewards + stakingRewards; + // Circulating supply = claimedTotalSupply - locked + uint256 claimedTotal = zkc.claimedTotalSupply(); + uint256 expectedCirculating = claimedTotal - supplyCalculator.locked(); uint256 circulatingSupply = supplyCalculator.circulatingSupply(); assertEq(circulatingSupply, expectedCirculating); + // Verify it matches: INITIAL_UNLOCKED + rewards + assertEq(circulatingSupply, INITIAL_UNLOCKED + povwRewards + stakingRewards); } function testUpdateUnlockedValue() public { uint256 newUnlocked = 750_000_000e18; // 750M tokens + uint256 expectedLocked = zkc.INITIAL_SUPPLY() - newUnlocked; vm.expectEmit(true, true, true, true); emit SupplyCalculator.UnlockedValueUpdated(INITIAL_UNLOCKED, newUnlocked); @@ -113,10 +123,14 @@ contract SupplyCalculatorTest is Test { supplyCalculator.updateUnlockedValue(newUnlocked); assertEq(supplyCalculator.unlocked(), newUnlocked); + assertEq(supplyCalculator.locked(), expectedLocked); + // Verify synchronization: locked + unlocked = INITIAL_SUPPLY + assertEq(supplyCalculator.locked() + supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY()); // Check circulating supply updated correctly uint256 circulatingSupply = supplyCalculator.circulatingSupply(); - assertEq(circulatingSupply, newUnlocked); + uint256 claimedTotal = zkc.claimedTotalSupply(); + assertEq(circulatingSupply, claimedTotal - supplyCalculator.locked()); } function testUpdateUnlockedValueAccessControl() public { @@ -131,6 +145,7 @@ contract SupplyCalculatorTest is Test { vm.prank(owner); supplyCalculator.updateUnlockedValue(newUnlocked); assertEq(supplyCalculator.unlocked(), newUnlocked); + assertEq(supplyCalculator.locked(), zkc.INITIAL_SUPPLY() - newUnlocked); } function testUpgradeAccessControl() public { @@ -161,9 +176,13 @@ contract SupplyCalculatorTest is Test { vm.prank(user); zkc.burn(burnAmount); - uint256 expectedCirculating = INITIAL_UNLOCKED + rewards - burnAmount; + // Circulating supply = claimedTotalSupply - locked + uint256 claimedTotal = zkc.claimedTotalSupply(); + uint256 expectedCirculating = claimedTotal - supplyCalculator.locked(); uint256 circulatingSupply = supplyCalculator.circulatingSupply(); assertEq(circulatingSupply, expectedCirculating); + // Verify it matches: INITIAL_UNLOCKED + rewards - burned + assertEq(circulatingSupply, INITIAL_UNLOCKED + rewards - burnAmount); } function testCirculatingSupplyRounded() public { @@ -233,4 +252,59 @@ contract SupplyCalculatorTest is Test { assertEq(rounded18dp, 1001234568000000000000000000); assertEq(roundedAmount, 1001234568); } + + function testUpdateLockedValue() public { + uint256 newLocked = 794_546_893e18; // 794,546,893 tokens + uint256 expectedUnlocked = zkc.INITIAL_SUPPLY() - newLocked; + + vm.expectEmit(true, true, true, true); + emit SupplyCalculator.LockedValueUpdated(supplyCalculator.locked(), newLocked); + vm.prank(owner); + supplyCalculator.updateLockedValue(newLocked); + + assertEq(supplyCalculator.locked(), newLocked); + assertEq(supplyCalculator.unlocked(), expectedUnlocked); + // Verify synchronization: locked + unlocked = INITIAL_SUPPLY + assertEq(supplyCalculator.locked() + supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY()); + + // Check circulating supply updated correctly + uint256 claimedTotal = zkc.claimedTotalSupply(); + uint256 circulatingSupply = supplyCalculator.circulatingSupply(); + assertEq(circulatingSupply, claimedTotal - newLocked); + } + + function testUpdateLockedValueAccessControl() public { + uint256 newLocked = 750_000_000e18; + + // Non-admin should not be able to update + vm.prank(user); + vm.expectRevert(); + supplyCalculator.updateLockedValue(newLocked); + + // Admin should be able to update + vm.prank(owner); + supplyCalculator.updateLockedValue(newLocked); + assertEq(supplyCalculator.locked(), newLocked); + assertEq(supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY() - newLocked); + } + + function testSynchronizationBetweenLockedAndUnlocked() public { + // Test that updating unlocked syncs locked + uint256 newUnlocked = 300_000_000e18; + vm.prank(owner); + supplyCalculator.updateUnlockedValue(newUnlocked); + + assertEq(supplyCalculator.unlocked(), newUnlocked); + assertEq(supplyCalculator.locked(), zkc.INITIAL_SUPPLY() - newUnlocked); + assertEq(supplyCalculator.locked() + supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY()); + + // Test that updating locked syncs unlocked + uint256 newLocked = 600_000_000e18; + vm.prank(owner); + supplyCalculator.updateLockedValue(newLocked); + + assertEq(supplyCalculator.locked(), newLocked); + assertEq(supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY() - newLocked); + assertEq(supplyCalculator.locked() + supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY()); + } } diff --git a/test/circulating/CirculatingZKC.t.sol b/test/circulating/CirculatingZKC.t.sol deleted file mode 100644 index c5427c1..0000000 --- a/test/circulating/CirculatingZKC.t.sol +++ /dev/null @@ -1,241 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import "forge-std/Test.sol"; -import "../../src/circulating/CirculatingZKC.sol"; -import "../../src/ZKC.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -contract CirculatingZKCTest is Test { - CirculatingZKC public circulatingZKC; - ZKC public zkc; - - address public owner = makeAddr("owner"); - address public minter1 = makeAddr("minter1"); - address public minter2 = makeAddr("minter2"); - address public povwMinter = makeAddr("povwMinter"); - address public stakingMinter = makeAddr("stakingMinter"); - address public user = makeAddr("user"); - - uint256 public constant INITIAL_UNLOCKED = 500_000_000e18; // 500M tokens initially unlocked - - function setUp() public { - // Deploy ZKC - deployZKC(); - - // Deploy CirculatingZKC - deployCirculatingZKC(); - } - - function deployZKC() internal { - // Deploy implementation - ZKC implementation = new ZKC(); - - // Deploy proxy and initialize - bytes memory initData = - abi.encodeWithSelector(ZKC.initialize.selector, minter1, minter2, implementation.INITIAL_SUPPLY(), 0, owner); - - ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); - zkc = ZKC(address(proxy)); - - // Initialize V2 and V3 - vm.prank(owner); - zkc.initializeV2(); - - vm.prank(owner); - zkc.initializeV3(); - - // Perform initial mint - address[] memory recipients = new address[](1); - recipients[0] = user; - uint256[] memory amounts = new uint256[](1); - amounts[0] = zkc.INITIAL_SUPPLY(); - vm.prank(minter1); - zkc.initialMint(recipients, amounts); - - // Grant minter roles - vm.startPrank(owner); - zkc.grantRole(zkc.POVW_MINTER_ROLE(), povwMinter); - zkc.grantRole(zkc.STAKING_MINTER_ROLE(), stakingMinter); - vm.stopPrank(); - } - - function deployCirculatingZKC() internal { - // Deploy implementation - CirculatingZKC implementation = new CirculatingZKC(); - - // Deploy proxy and initialize - bytes memory initData = - abi.encodeWithSelector(CirculatingZKC.initialize.selector, address(zkc), INITIAL_UNLOCKED, owner); - - ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); - circulatingZKC = CirculatingZKC(address(proxy)); - } - - function testInitialization() public view { - assertEq(address(circulatingZKC.zkc()), address(zkc)); - assertEq(circulatingZKC.unlocked(), INITIAL_UNLOCKED); - assertEq(circulatingZKC.locked(), zkc.INITIAL_SUPPLY() - INITIAL_UNLOCKED); - assertTrue(circulatingZKC.hasRole(circulatingZKC.ADMIN_ROLE(), owner)); - } - - function testCirculatingSupplyAfterInitialMint() public { - // Circulating supply = claimedTotalSupply - locked - // claimedTotalSupply = INITIAL_SUPPLY (1B), locked = INITIAL_SUPPLY - INITIAL_UNLOCKED - uint256 claimedTotal = zkc.claimedTotalSupply(); - uint256 expectedCirculating = claimedTotal - circulatingZKC.locked(); - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); - assertEq(circulatingSupply, expectedCirculating); - assertEq(circulatingSupply, INITIAL_UNLOCKED); - } - - function testCirculatingSupplyAfterRewardsMinted() public { - // Skip forward in time to simulate epochs passing - vm.warp(block.timestamp + 4 weeks); - - // Mint some PoVW rewards - uint256 povwRewards = 1_000_000e18; // 1M tokens - vm.prank(povwMinter); - zkc.mintPoVWRewardsForRecipient(user, povwRewards); - - // Mint some staking rewards - uint256 stakingRewards = 500_000e18; // 500K tokens - vm.prank(stakingMinter); - zkc.mintStakingRewardsForRecipient(user, stakingRewards); - - // Circulating supply = claimedTotalSupply - locked - uint256 claimedTotal = zkc.claimedTotalSupply(); - uint256 expectedCirculating = claimedTotal - circulatingZKC.locked(); - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); - assertEq(circulatingSupply, expectedCirculating); - // Verify it matches: INITIAL_UNLOCKED + rewards - assertEq(circulatingSupply, INITIAL_UNLOCKED + povwRewards + stakingRewards); - } - - function testUpdateUnlockedValue() public { - uint256 newUnlocked = 750_000_000e18; // 750M tokens - uint256 expectedLocked = zkc.INITIAL_SUPPLY() - newUnlocked; - - vm.expectEmit(true, true, true, true); - emit CirculatingZKC.UnlockedValueUpdated(INITIAL_UNLOCKED, newUnlocked); - vm.prank(owner); - circulatingZKC.updateUnlockedValue(newUnlocked); - - assertEq(circulatingZKC.unlocked(), newUnlocked); - assertEq(circulatingZKC.locked(), expectedLocked); - // Verify synchronization: locked + unlocked = INITIAL_SUPPLY - assertEq(circulatingZKC.locked() + circulatingZKC.unlocked(), zkc.INITIAL_SUPPLY()); - - // Check circulating supply updated correctly - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); - uint256 claimedTotal = zkc.claimedTotalSupply(); - assertEq(circulatingSupply, claimedTotal - circulatingZKC.locked()); - } - - function testUpdateUnlockedValueAccessControl() public { - uint256 newUnlocked = 750_000_000e18; - - // Non-admin should not be able to update - vm.prank(user); - vm.expectRevert(); - circulatingZKC.updateUnlockedValue(newUnlocked); - - // Admin should be able to update - vm.prank(owner); - circulatingZKC.updateUnlockedValue(newUnlocked); - assertEq(circulatingZKC.unlocked(), newUnlocked); - assertEq(circulatingZKC.locked(), zkc.INITIAL_SUPPLY() - newUnlocked); - } - - function testUpgradeAccessControl() public { - // Deploy new implementation - CirculatingZKC newImplementation = new CirculatingZKC(); - - // Non-admin should not be able to upgrade - vm.prank(user); - vm.expectRevert(); - circulatingZKC.upgradeToAndCall(address(newImplementation), ""); - - // Admin should be able to upgrade - vm.prank(owner); - circulatingZKC.upgradeToAndCall(address(newImplementation), ""); - } - - function testCirculatingSupplyWithBurnedTokens() public { - // Skip forward in time to simulate epochs passing - vm.warp(block.timestamp + 4 weeks); - - // Mint some rewards - uint256 rewards = 1_000_000e18; - vm.prank(povwMinter); - zkc.mintPoVWRewardsForRecipient(user, rewards); - - // Burn some tokens - uint256 burnAmount = 500_000e18; - vm.prank(user); - zkc.burn(burnAmount); - - // Circulating supply = claimedTotalSupply - locked - uint256 claimedTotal = zkc.claimedTotalSupply(); - uint256 expectedCirculating = claimedTotal - circulatingZKC.locked(); - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); - assertEq(circulatingSupply, expectedCirculating); - // Verify it matches: INITIAL_UNLOCKED + rewards - burned - assertEq(circulatingSupply, INITIAL_UNLOCKED + rewards - burnAmount); - } - - function testUpdateLockedValue() public { - uint256 newLocked = 794_546_893e18; // 794,546,893 tokens - uint256 expectedUnlocked = zkc.INITIAL_SUPPLY() - newLocked; - - vm.expectEmit(true, true, true, true); - emit CirculatingZKC.LockedValueUpdated(circulatingZKC.locked(), newLocked); - vm.prank(owner); - circulatingZKC.updateLockedValue(newLocked); - - assertEq(circulatingZKC.locked(), newLocked); - assertEq(circulatingZKC.unlocked(), expectedUnlocked); - // Verify synchronization: locked + unlocked = INITIAL_SUPPLY - assertEq(circulatingZKC.locked() + circulatingZKC.unlocked(), zkc.INITIAL_SUPPLY()); - - // Check circulating supply updated correctly - uint256 claimedTotal = zkc.claimedTotalSupply(); - uint256 circulatingSupply = circulatingZKC.circulatingSupply(); - assertEq(circulatingSupply, claimedTotal - newLocked); - } - - function testUpdateLockedValueAccessControl() public { - uint256 newLocked = 750_000_000e18; - - // Non-admin should not be able to update - vm.prank(user); - vm.expectRevert(); - circulatingZKC.updateLockedValue(newLocked); - - // Admin should be able to update - vm.prank(owner); - circulatingZKC.updateLockedValue(newLocked); - assertEq(circulatingZKC.locked(), newLocked); - assertEq(circulatingZKC.unlocked(), zkc.INITIAL_SUPPLY() - newLocked); - } - - function testSynchronizationBetweenLockedAndUnlocked() public { - // Test that updating unlocked syncs locked - uint256 newUnlocked = 300_000_000e18; - vm.prank(owner); - circulatingZKC.updateUnlockedValue(newUnlocked); - - assertEq(circulatingZKC.unlocked(), newUnlocked); - assertEq(circulatingZKC.locked(), zkc.INITIAL_SUPPLY() - newUnlocked); - assertEq(circulatingZKC.locked() + circulatingZKC.unlocked(), zkc.INITIAL_SUPPLY()); - - // Test that updating locked syncs unlocked - uint256 newLocked = 600_000_000e18; - vm.prank(owner); - circulatingZKC.updateLockedValue(newLocked); - - assertEq(circulatingZKC.locked(), newLocked); - assertEq(circulatingZKC.unlocked(), zkc.INITIAL_SUPPLY() - newLocked); - assertEq(circulatingZKC.locked() + circulatingZKC.unlocked(), zkc.INITIAL_SUPPLY()); - } -} From c9d59cb7216a377f2dd788c642310a2fb553072d Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:15:37 -0600 Subject: [PATCH 09/11] Update script and toml --- deployment.toml | 5 +- script/Upgrade.s.sol | 126 +++++++++++++++++++++++++++ script/manage | 54 +++++++++++- src/calculators/SupplyCalculator.sol | 16 ++++ update_deployment_toml.py | 2 + 5 files changed, 199 insertions(+), 4 deletions(-) diff --git a/deployment.toml b/deployment.toml index c17c773..af79c88 100644 --- a/deployment.toml +++ b/deployment.toml @@ -26,13 +26,14 @@ staking-rewards-deployer = "0xb13573c6ceb505a7bdd4fa3ad7b473c5c5d36b19" povw-minter = "0xbfce7c2d5e7eddeab71b3eeed770713c8b755397" staking-minter = "0x459d87d54808fac136ddcf439fcc1d8a238311c7" supply-calculator = "0x15eB8cA378E33381c867573EF2f25Ca1010f866d" -supply-calculator-impl = "0x1ff6f81ea7f5e6feafaafb0fc5983576096ded1d" +supply-calculator-impl = "0xddac1f7395815a8afd6673a98f5eddd31b99acd2" supply-calculator-admin = "0xb13573c6ceb505a7bdd4fa3ad7b473c5c5d36b19" supply-calculator-admin-2 = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" -supply-calculator-commit = "537c077" +supply-calculator-commit = "2458b44" zkc-commit = "5b8310b" vezkc-commit = "caa29df" staking-rewards-commit = "bac0f29" +supply-calculator-impl-prev = "0x1ff6f81ea7f5e6feafaafb0fc5983576096ded1d" [deployment.ethereum-sepolia] name = "Ethereum Sepolia" diff --git a/script/Upgrade.s.sol b/script/Upgrade.s.sol index 0e7fba9..1793ff0 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -542,3 +542,129 @@ contract UpgradeSupplyCalculator is BaseDeployment { console2.log("New Implementation: ", newImpl); } } + +/** + * Sample Usage for SupplyCalculator upgrade with initializeV2: + * + * # Option 1: Safe upgrade with reference build (recommended) + * # First, create reference build from deployed SupplyCalculator commit: + * export DEPLOYED_COMMIT=$(python3 -c "import tomlkit; print(tomlkit.load(open('deployment.toml'))['deployment']['$CHAIN_KEY']['supply-calculator-commit'])") + * WORKTREE_PATH="../supply-calculator-reference-${DEPLOYED_COMMIT}" + * git worktree add "$WORKTREE_PATH" "$DEPLOYED_COMMIT" + * cd "$WORKTREE_PATH" + * forge build --profile reference + * cp -R out-reference/build-info "$OLDPWD/build-info-reference" + * cd "$OLDPWD" + * + * # Then run upgrade: + * export CHAIN_KEY="anvil" + * export INITIAL_LOCKED=500000000000000000000000000 # 500M ZKC in wei + * forge script script/Upgrade.s.sol:UpgradeSupplyCalculatorInitV2 \ + * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + * --broadcast \ + * --rpc-url http://127.0.0.1:8545 + */ +contract UpgradeSupplyCalculatorInitV2 is BaseDeployment { + function run() public { + (DeploymentConfig memory config, string memory deploymentKey) = ConfigLoader.loadDeploymentConfig(vm); + require(config.supplyCalculator != address(0), "SupplyCalculator not deployed"); + + // Read initial locked value from environment variable + uint256 initialLocked = vm.envUint("INITIAL_LOCKED"); + console2.log("Initial locked value: ", initialLocked); + console2.log("Initial locked (in tokens): ", initialLocked / 10 ** 18); + + vm.startBroadcast(); + + // Check for skip safety checks flag + bool skipSafetyChecks = vm.envOr("SKIP_SAFETY_CHECKS", false); + + // Prepare upgrade options with reference contract + Options memory opts; + + if (skipSafetyChecks) { + console2.log("WARNING: Skipping all upgrade safety checks and reference build!"); + opts.unsafeSkipAllChecks = true; + } else { + // Get the SupplyCalculator commit hash from deployment config for commit-specific reference build + string memory supplyCalculatorCommit = config.supplyCalculatorCommit; + require( + bytes(supplyCalculatorCommit).length > 0, "SupplyCalculator commit hash not found in deployment config" + ); + + string memory referenceBuildDir = string.concat("build-info-reference-", supplyCalculatorCommit); + opts.referenceContract = string.concat(referenceBuildDir, ":SupplyCalculator"); + opts.referenceBuildInfoDir = referenceBuildDir; + console2.log("Using reference build directory: ", referenceBuildDir); + } + + address currentImpl = _getImplementationAddress(config.supplyCalculator); + bytes memory initializerData = abi.encodeCall(SupplyCalculator.initializeV2, (initialLocked)); + address newImpl; + + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + + if (gnosisExecute) { + console2.log("GNOSIS_EXECUTE=true: Deploying new implementation for Safe upgrade"); + console2.log("Target proxy address: ", config.supplyCalculator); + console2.log("Current implementation: ", currentImpl); + + // Use prepareUpgrade for validation + deployment + newImpl = Upgrades.prepareUpgrade("SupplyCalculator.sol:SupplyCalculator", opts); + console2.log("New implementation deployed: ", newImpl); + + // Print Gnosis Safe transaction info + _printGnosisSafeInfo(config.supplyCalculator, newImpl, initializerData); + } else { + console2.log("Upgrading SupplyCalculator at: ", config.supplyCalculator); + console2.log("Current implementation: ", currentImpl); + + // Perform upgrade with initializeV2 initializer + Upgrades.upgradeProxy(config.supplyCalculator, "SupplyCalculator.sol:SupplyCalculator", initializerData, opts); + + newImpl = Upgrades.getImplementationAddress(config.supplyCalculator); + console2.log("Upgraded SupplyCalculator implementation to: ", newImpl); + } + + vm.stopBroadcast(); + + // Update deployment.toml with new implementation and store previous + _updateDeploymentConfig(deploymentKey, "supply-calculator-impl-prev", currentImpl); + _updateDeploymentConfig(deploymentKey, "supply-calculator-impl", newImpl); + _updateSupplyCalculatorCommit(deploymentKey); + + // Print Gnosis Safe transaction info (only if not in GNOSIS_EXECUTE mode) + if (!gnosisExecute) { + _printGnosisSafeInfo(config.supplyCalculator, newImpl, initializerData); + } + + // Verify results + if (gnosisExecute) { + console2.log("================================================"); + console2.log("SupplyCalculator Implementation Deployment Complete"); + console2.log("New Implementation: ", newImpl); + console2.log("Proxy NOT upgraded - use Gnosis Safe to complete upgrade"); + console2.log("Note: initializeV2 will be called when Safe executes upgrade"); + } else { + SupplyCalculator supplyCalculatorContract = SupplyCalculator(config.supplyCalculator); + console2.log( + "Proxy still points to SupplyCalculator: ", address(supplyCalculatorContract) == config.supplyCalculator + ); + console2.log("Implementation updated: ", newImpl != config.supplyCalculatorImpl); + console2.log("ZKC token still configured: ", address(supplyCalculatorContract.zkc()) == config.zkc); + console2.log("Claimed total supply: ", supplyCalculatorContract.zkc().claimedTotalSupply()); + console2.log("Claimed total supply (in tokens): ", supplyCalculatorContract.zkc().claimedTotalSupply() / 10 ** 18); + console2.log("Locked value: ", supplyCalculatorContract.locked()); + console2.log("Locked value (in tokens): ", supplyCalculatorContract.locked() / 10 ** 18); + console2.log("Unlocked value: ", supplyCalculatorContract.unlocked()); + console2.log("Unlocked value (in tokens): ", supplyCalculatorContract.unlocked() / 10 ** 18); + console2.log("Circulating supply: ", supplyCalculatorContract.circulatingSupply()); + console2.log("Circulating supply (in tokens): ", supplyCalculatorContract.circulatingSupply() / 10 ** 18); + console2.log("initializeV2 called during upgrade"); + console2.log("================================================"); + console2.log("SupplyCalculator Upgrade with InitV2 Complete"); + console2.log("New Implementation: ", newImpl); + } + } +} diff --git a/script/manage b/script/manage index 7137cf5..e67e616 100755 --- a/script/manage +++ b/script/manage @@ -7,6 +7,11 @@ load_env_var() { local var_name="$2" local file="${3:-deployment.toml}" + # If environment variable is already set, don't override it + if [[ -n "${!var_name:-}" ]]; then + return + fi + # Try secrets file first if it exists if [[ -f "deployment_secrets.toml" ]] && [[ "$file" == "deployment_secrets.toml" ]]; then value=$(python3 -c " @@ -267,6 +272,7 @@ Commands: upgrade-vezkc Upgrade veZKC implementation upgrade-staking Upgrade StakingRewards implementation upgrade-supply-calculator Upgrade SupplyCalculator implementation + upgrade-supply-calculator-initv2 Upgrade SupplyCalculator and call initializeV2 rollback-zkc Rollback ZKC to previous implementation rollback-vezkc Rollback veZKC to previous implementation rollback-staking Rollback StakingRewards to previous implementation @@ -298,6 +304,7 @@ Environment Variables: PRIVATE_KEY Required. Private key for transaction signing (0x...) GNOSIS_EXECUTE Optional. Set to 'true' to generate Gnosis Safe calldata without executing transactions SKIP_SAFETY_CHECKS Optional. Set to 'true' to skip upgrade safety checks (WARNING: unsafe!) + INITIAL_LOCKED Required for upgrade-supply-calculator-initv2. Locked value in wei (e.g., 794546893000000000000000000) ADMIN_TO_ADD Required for add-*-admin commands. Admin address to grant role to (0x...) ADMIN_TO_REMOVE Required for remove-*-admin commands. Admin address to revoke role from (0x...) @@ -314,6 +321,9 @@ Examples: ADMIN="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" \ script/manage deploy-zkc --broadcast + # Upgrade SupplyCalculator and call initializeV2 + PRIVATE_KEY=0x... INITIAL_LOCKED=794546893000000000000000000 CHAIN_KEY=anvil ./script/manage upgrade-supply-calculator-initv2 --broadcast + # Deploy veZKC to sepolia with Fireblocks (requires ZKC already deployed) PRIVATE_KEY=0x... CHAIN_KEY=ethereum-sepolia ./script/manage deploy-vezkc --fireblocks --broadcast --verify @@ -340,6 +350,9 @@ Examples: # Upgrade SupplyCalculator on mainnet with verification PRIVATE_KEY=0x... CHAIN_KEY=ethereum-mainnet ./script/manage upgrade-supply-calculator --broadcast --verify + + # Upgrade SupplyCalculator and call initializeV2 + PRIVATE_KEY=0x... INITIAL_LOCKED=794546893000000000000000000 CHAIN_KEY=anvil ./script/manage upgrade-supply-calculator-initv2 --broadcast # Rollback ZKC to previous implementation PRIVATE_KEY=0x... CHAIN_KEY=anvil ./script/manage rollback-zkc --broadcast @@ -466,6 +479,12 @@ main() { load_env_var "etherscan-api-key" "ETHERSCAN_API_KEY" "deployment_secrets.toml" load_env_var "id" "CHAIN_ID" + # Use ETH_RPC_URL as fallback if RPC_URL is not set + if [[ -z "${RPC_URL:-}" ]] && [[ -n "${ETH_RPC_URL:-}" ]]; then + RPC_URL="$ETH_RPC_URL" + echo "📝 Using ETH_RPC_URL as RPC_URL" + fi + # PRIVATE_KEY must be set as environment variable if [[ -z "${PRIVATE_KEY:-}" ]]; then echo "❌ PRIVATE_KEY environment variable is required" @@ -481,6 +500,7 @@ main() { # Verify required configuration if [[ -z "${RPC_URL:-}" ]]; then echo "❌ RPC URL not configured for $CHAIN_KEY" + echo " Set RPC_URL or ETH_RPC_URL environment variable, or configure in deployment_secrets.toml" exit 1 fi @@ -491,7 +511,34 @@ main() { # Verify chain ID matches RPC echo "🔍 Verifying chain configuration..." - actual_chain_id=$(cast chain-id --rpc-url "$RPC_URL" 2>/dev/null || echo "") + echo " RPC URL: $RPC_URL" + echo " Expected Chain ID: $CHAIN_ID" + echo " Querying RPC..." + + actual_chain_id=$(cast chain-id --rpc-url "$RPC_URL" 2>&1) + cast_exit_code=$? + + if [[ $cast_exit_code -ne 0 ]]; then + echo "❌ Failed to query chain ID from RPC:" + echo " Exit code: $cast_exit_code" + echo " Error output: $actual_chain_id" + echo "" + echo " Troubleshooting:" + echo " - Verify your RPC URL is correct and accessible" + echo " - Check your network connection" + echo " - Ensure the RPC endpoint is responding" + exit 1 + fi + + if [[ -z "$actual_chain_id" ]]; then + echo "❌ Chain ID query returned empty result" + echo " Please verify your RPC URL is correct and accessible" + exit 1 + fi + + # Trim whitespace from chain ID + actual_chain_id=$(echo "$actual_chain_id" | xargs) + if [[ "$actual_chain_id" != "$CHAIN_ID" ]]; then echo "❌ Chain ID mismatch:" echo " Expected: $CHAIN_ID (from deployment.toml)" @@ -518,7 +565,7 @@ main() { upgrade-staking) contract_type="staking" ;; - upgrade-supply-calculator) + upgrade-supply-calculator|upgrade-supply-calculator-initv2) contract_type="supply-calculator" ;; *) @@ -570,6 +617,9 @@ main() { upgrade-supply-calculator) forge_script "UpgradeSupplyCalculator" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; + upgrade-supply-calculator-initv2) + forge_script "UpgradeSupplyCalculatorInitV2" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" + ;; rollback-zkc) forge_script "RollbackZKC" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; diff --git a/src/calculators/SupplyCalculator.sol b/src/calculators/SupplyCalculator.sol index cc2c283..35116e9 100644 --- a/src/calculators/SupplyCalculator.sol +++ b/src/calculators/SupplyCalculator.sol @@ -56,6 +56,22 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad _grantRole(ADMIN_ROLE, _admin); } + /// @notice Initialize V2: Set locked value directly + /// @param newLocked Initial value for the locked tokens + /// @dev Sets locked to specified value and unlocked to INITIAL_SUPPLY - locked + function initializeV2(uint256 newLocked) public reinitializer(2) { + require(newLocked <= Supply.INITIAL_SUPPLY, "Locked cannot exceed initial supply"); + + uint256 oldLocked = locked; + uint256 oldUnlocked = unlocked; + + locked = newLocked; + unlocked = Supply.INITIAL_SUPPLY - newLocked; + + emit LockedValueUpdated(oldLocked, locked); + emit UnlockedValueUpdated(oldUnlocked, unlocked); + } + /// @notice Calculate the current circulating supply /// @dev Formula: claimedTotalSupply - locked /// @return The current circulating supply of ZKC tokens diff --git a/update_deployment_toml.py b/update_deployment_toml.py index 7fa9502..58990f6 100755 --- a/update_deployment_toml.py +++ b/update_deployment_toml.py @@ -50,6 +50,7 @@ def main(): parser.add_argument('--staking-minter', help='Staking minter address') parser.add_argument('--supply-calculator', help='SupplyCalculator proxy address') parser.add_argument('--supply-calculator-impl', help='SupplyCalculator implementation address') + parser.add_argument('--supply-calculator-impl-prev', help='Previous SupplyCalculator implementation address') parser.add_argument('--supply-calculator-admin', help='SupplyCalculator admin address') # Deployment metadata @@ -119,6 +120,7 @@ def main(): 'staking_minter': args.staking_minter, 'supply_calculator': args.supply_calculator, 'supply_calculator_impl': args.supply_calculator_impl, + 'supply_calculator_impl_prev': args.supply_calculator_impl_prev, 'supply_calculator_admin': args.supply_calculator_admin, 'zkc_commit': args.zkc_commit, 'vezkc_commit': args.vezkc_commit, From f3f0c2a28b917d800790c66c3ac10bc19b002984 Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:09:26 -0500 Subject: [PATCH 10/11] Trigger actions --- script/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/README.md b/script/README.md index 70876ff..0548f7a 100644 --- a/script/README.md +++ b/script/README.md @@ -1,4 +1,4 @@ -# ZKC Scripts Guide +# ZKC Scripts ## Prerequisites From 055331dd0ccb018631ad7be4178078f6925cb0b2 Mon Sep 17 00:00:00 2001 From: Pote <81638931+willpote@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:10:44 -0500 Subject: [PATCH 11/11] Forge fmt --- script/Upgrade.s.sol | 8 ++++++-- src/calculators/SupplyCalculator.sol | 6 +++--- test/calculators/SupplyCalculator.t.sol | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/script/Upgrade.s.sol b/script/Upgrade.s.sol index 1793ff0..20e4471 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -621,7 +621,9 @@ contract UpgradeSupplyCalculatorInitV2 is BaseDeployment { console2.log("Current implementation: ", currentImpl); // Perform upgrade with initializeV2 initializer - Upgrades.upgradeProxy(config.supplyCalculator, "SupplyCalculator.sol:SupplyCalculator", initializerData, opts); + Upgrades.upgradeProxy( + config.supplyCalculator, "SupplyCalculator.sol:SupplyCalculator", initializerData, opts + ); newImpl = Upgrades.getImplementationAddress(config.supplyCalculator); console2.log("Upgraded SupplyCalculator implementation to: ", newImpl); @@ -654,7 +656,9 @@ contract UpgradeSupplyCalculatorInitV2 is BaseDeployment { console2.log("Implementation updated: ", newImpl != config.supplyCalculatorImpl); console2.log("ZKC token still configured: ", address(supplyCalculatorContract.zkc()) == config.zkc); console2.log("Claimed total supply: ", supplyCalculatorContract.zkc().claimedTotalSupply()); - console2.log("Claimed total supply (in tokens): ", supplyCalculatorContract.zkc().claimedTotalSupply() / 10 ** 18); + console2.log( + "Claimed total supply (in tokens): ", supplyCalculatorContract.zkc().claimedTotalSupply() / 10 ** 18 + ); console2.log("Locked value: ", supplyCalculatorContract.locked()); console2.log("Locked value (in tokens): ", supplyCalculatorContract.locked() / 10 ** 18); console2.log("Unlocked value: ", supplyCalculatorContract.unlocked()); diff --git a/src/calculators/SupplyCalculator.sol b/src/calculators/SupplyCalculator.sol index 35116e9..a6ded95 100644 --- a/src/calculators/SupplyCalculator.sol +++ b/src/calculators/SupplyCalculator.sol @@ -61,13 +61,13 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad /// @dev Sets locked to specified value and unlocked to INITIAL_SUPPLY - locked function initializeV2(uint256 newLocked) public reinitializer(2) { require(newLocked <= Supply.INITIAL_SUPPLY, "Locked cannot exceed initial supply"); - + uint256 oldLocked = locked; uint256 oldUnlocked = unlocked; - + locked = newLocked; unlocked = Supply.INITIAL_SUPPLY - newLocked; - + emit LockedValueUpdated(oldLocked, locked); emit UnlockedValueUpdated(oldUnlocked, unlocked); } diff --git a/test/calculators/SupplyCalculator.t.sol b/test/calculators/SupplyCalculator.t.sol index 5bb0552..5a80817 100644 --- a/test/calculators/SupplyCalculator.t.sol +++ b/test/calculators/SupplyCalculator.t.sol @@ -293,7 +293,7 @@ contract SupplyCalculatorTest is Test { uint256 newUnlocked = 300_000_000e18; vm.prank(owner); supplyCalculator.updateUnlockedValue(newUnlocked); - + assertEq(supplyCalculator.unlocked(), newUnlocked); assertEq(supplyCalculator.locked(), zkc.INITIAL_SUPPLY() - newUnlocked); assertEq(supplyCalculator.locked() + supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY()); @@ -302,7 +302,7 @@ contract SupplyCalculatorTest is Test { uint256 newLocked = 600_000_000e18; vm.prank(owner); supplyCalculator.updateLockedValue(newLocked); - + assertEq(supplyCalculator.locked(), newLocked); assertEq(supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY() - newLocked); assertEq(supplyCalculator.locked() + supplyCalculator.unlocked(), zkc.INITIAL_SUPPLY());