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 2154334..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-commit = "537c077" +supply-calculator-admin-2 = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" +supply-calculator-commit = "2458b44" zkc-commit = "5b8310b" vezkc-commit = "caa29df" staking-rewards-commit = "bac0f29" -supply-calculator-admin-2 = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" +supply-calculator-impl-prev = "0x1ff6f81ea7f5e6feafaafb0fc5983576096ded1d" [deployment.ethereum-sepolia] name = "Ethereum Sepolia" 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 diff --git a/script/Update.s.sol b/script/Update.s.sol index 3921d7e..b2bd03b 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,23 +211,23 @@ 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 \ * --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ * --rpc-url http://127.0.0.1:8545 */ -contract UpdateSupplyCalculatorUnlocked is BaseDeployment { +contract UpdateSupplyCalculatorLocked 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"); - // 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,23 +237,31 @@ 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); 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 circulating supply for display + // Calculate expected new values for display 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: ", newUnlocked); console2.log("New unlocked amount (in tokens): ", newUnlocked / 10 ** 18); console2.log("New circulating supply: ", expectedNewCirculatingSupply); @@ -261,51 +270,54 @@ contract UpdateSupplyCalculatorUnlocked is BaseDeployment { } 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(); + 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"); } } } /// @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 ==="); 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/script/Upgrade.s.sol b/script/Upgrade.s.sol index 8fb6e05..20e4471 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -535,8 +535,140 @@ 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); } } + +/** + * 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 52711ef..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[@]}"}" ;; @@ -592,7 +642,7 @@ main() { forge_script "RemovePOVWMinter" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; update-supply-calculator) - forge_script "UpdateSupplyCalculatorUnlocked" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" + forge_script "UpdateSupplyCalculatorLocked" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" ;; add-zkc-admin) forge_script "AddZKCAdmin" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}" diff --git a/src/calculators/SupplyCalculator.sol b/src/calculators/SupplyCalculator.sol index 76481dc..a6ded95 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,32 @@ contract SupplyCalculator is Initializable, AccessControlUpgradeable, UUPSUpgrad zkc = IZKC(_zkc); unlocked = _initialUnlocked; + locked = Supply.INITIAL_SUPPLY - _initialUnlocked; _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: 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 +179,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/test/.DS_Store b/test/.DS_Store new file mode 100644 index 0000000..307b4c4 Binary files /dev/null and b/test/.DS_Store differ diff --git a/test/calculators/SupplyCalculator.t.sol b/test/calculators/SupplyCalculator.t.sol index 5322170..5a80817 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/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,