From d4d62b0d11d370d169d76ebe74067e0158b52d64 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Tue, 14 Apr 2026 08:47:46 +0000 Subject: [PATCH 1/5] fix(scripts): harden operator key management security - Remove private key logging from create-operator-keystore and get-operator-key; address is sufficient for confirmation - Remove staking provider and operator private keys from generated .env files; keys written to disk are a git-leak risk - Print staking provider key once to terminal with a prominent "copy now" warning instead of persisting it to .env - Require non-empty password in create-operator-keystore and setup-new-staking-provider; empty-password keystores are trivially decryptable - Require explicit keystore path in get-operator-key; remove the hardcoded developer-machine UUID default that caused ENOENT for all other users - Fix --list path in get-operator-key: ../../operator-1-keystore resolved above repo root; corrected to ../operator-1-keystore - Convert sync fs calls to fs.promises and add try/catch inside main() across all three scripts --- scripts/create-operator-keystore.js | 38 ++++++----- scripts/get-operator-key.js | 56 +++++++++++------ scripts/setup-new-staking-provider.js | 90 +++++++++++++++------------ 3 files changed, 109 insertions(+), 75 deletions(-) diff --git a/scripts/create-operator-keystore.js b/scripts/create-operator-keystore.js index 09250cdb..00d80c13 100644 --- a/scripts/create-operator-keystore.js +++ b/scripts/create-operator-keystore.js @@ -3,27 +3,31 @@ const fs = require("fs"); const path = require("path"); const { ethers } = require("ethers"); -const password = process.argv[2] || ""; +const password = process.argv[2]; +if (!password) { + console.error("Usage: node create-operator-keystore.js "); + console.error("A non-empty password is required to encrypt the keystore."); + process.exit(1); +} async function main() { - const wallet = ethers.Wallet.createRandom(); - const encrypted = await wallet.encrypt(password); + try { + const wallet = ethers.Wallet.createRandom(); + const encrypted = await wallet.encrypt(password); - const dir = path.join(__dirname, "../operator-1-keystore"); - const filename = wallet.address.toLowerCase().slice(2, 10) + "-new-operator"; - const filepath = path.join(dir, filename); + const dir = path.join(__dirname, "../operator-1-keystore"); + const filename = wallet.address.toLowerCase().slice(2, 10) + "-new-operator"; + const filepath = path.join(dir, filename); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - fs.writeFileSync(filepath, encrypted); + await fs.promises.mkdir(dir, { recursive: true }); + await fs.promises.writeFile(filepath, encrypted); - console.log("Address:", wallet.address); - console.log("Private key:", wallet.privateKey); - console.log("Keystore saved to:", filepath); + console.log("Address:", wallet.address); + console.log("Keystore saved to:", filepath); + } catch (e) { + console.error(e); + process.exit(1); + } } -main().catch((e) => { - console.error(e); - process.exit(1); -}); +main(); diff --git a/scripts/get-operator-key.js b/scripts/get-operator-key.js index 5c2837a6..b29e065c 100644 --- a/scripts/get-operator-key.js +++ b/scripts/get-operator-key.js @@ -3,29 +3,47 @@ const fs = require("fs"); const path = require("path"); const { ethers } = require("ethers"); -const keystorePath = process.argv[2] || path.join(__dirname, "../operator-1-keystore/c88df450-36e6-473e-908a-3349242f463e"); +if (!process.argv[2]) { + console.error("Usage: node get-operator-key.js [password]"); + console.error(" node get-operator-key.js --list"); + process.exit(1); +} + +const keystorePath = process.argv[2]; const password = process.argv[3] || ""; -// If --list, show addresses for all keystores in operator-1-keystore -if (process.argv[2] === "--list") { - const dir = path.join(__dirname, "../../operator-1-keystore"); - const files = fs.readdirSync(dir).filter((f) => !f.startsWith(".") && f.length > 10); - for (const file of files) { - try { - let content = fs.readFileSync(path.join(dir, file), "utf8"); - const json = content.split("\n")[0]; // some files have extra content - if (json.startsWith("{")) { - const wallet = ethers.Wallet.fromEncryptedJsonSync(json, ""); - console.log(file, "->", wallet.address); +async function main() { + try { + if (keystorePath === "--list") { + const dir = path.join(__dirname, "../operator-1-keystore"); + const files = (await fs.promises.readdir(dir)).filter( + (f) => !f.startsWith(".") && f.length > 10 + ); + for (const file of files) { + try { + const content = await fs.promises.readFile( + path.join(dir, file), + "utf8" + ); + const json = content.split("\n")[0]; // some files have extra content + if (json.startsWith("{")) { + const wallet = await ethers.Wallet.fromEncryptedJson(json, ""); + console.log(file, "->", wallet.address); + } + } catch (e) { + console.log(file, "-> error:", e.message); + } } - } catch (e) { - console.log(file, "-> error:", e.message); + return; } + + const json = await fs.promises.readFile(keystorePath, "utf8"); + const wallet = await ethers.Wallet.fromEncryptedJson(json, password); + console.log("Address:", wallet.address); + } catch (e) { + console.error(e); + process.exit(1); } - process.exit(0); } -const json = fs.readFileSync(keystorePath, "utf8"); -const wallet = ethers.Wallet.fromEncryptedJsonSync(json, password); -console.log("Address:", wallet.address); -console.log("Private key:", wallet.privateKey); +main(); diff --git a/scripts/setup-new-staking-provider.js b/scripts/setup-new-staking-provider.js index 9ea10bbe..35def3e9 100644 --- a/scripts/setup-new-staking-provider.js +++ b/scripts/setup-new-staking-provider.js @@ -3,14 +3,14 @@ * Creates a new staking provider wallet and operator keystore for Threshold/Keep operator setup. * * Usage: - * node scripts/setup-new-staking-provider.js [operator-keystore-password] [operator-index] + * node scripts/setup-new-staking-provider.js [operator-index] * * If operator-index is a number (e.g. 1..8), writes .env.operator-{index} and a uniquely named keystore * instead of .env.new-operator (for multi-operator setups). * * Output: - * - Staking provider: address + private key (use for stake, authorize, registerOperator) - * - Operator: address + keystore file (use for keep-client and joinSortitionPool) + * - Staking provider: address only (private key is NOT written to disk -- store it yourself) + * - Operator: address + encrypted keystore file (use for keep-client and joinSortitionPool) * * Prerequisites: * - 80,000 T tokens (40k for RandomBeacon + 40k for WalletRegistry) @@ -20,7 +20,15 @@ const fs = require("fs"); const path = require("path"); const { ethers } = require("ethers"); -const operatorPassword = process.argv[2] || ""; +const operatorPassword = process.argv[2]; +if (!operatorPassword) { + console.error( + "Usage: node setup-new-staking-provider.js [operator-index]" + ); + console.error("A non-empty password is required to encrypt the operator keystore."); + process.exit(1); +} + const indexArg = process.argv[3]; const opIndex = indexArg !== undefined && /^[1-9][0-9]*$/.test(String(indexArg)) @@ -28,54 +36,58 @@ const opIndex = : null; async function main() { - const stakingProvider = ethers.Wallet.createRandom(); - const operator = ethers.Wallet.createRandom(); - const operatorEncrypted = await operator.encrypt(operatorPassword); + try { + const stakingProvider = ethers.Wallet.createRandom(); + const operator = ethers.Wallet.createRandom(); + const operatorEncrypted = await operator.encrypt(operatorPassword); - const keystoreDir = path.join(__dirname, "../operator-1-keystore"); - const operatorFilename = opIndex - ? `op${opIndex}-${operator.address.toLowerCase().slice(2, 10)}` - : operator.address.toLowerCase().slice(2, 10) + "-new-operator"; - const operatorFilepath = path.join(keystoreDir, operatorFilename); + const keystoreDir = path.join(__dirname, "../operator-1-keystore"); + const operatorFilename = opIndex + ? `op${opIndex}-${operator.address.toLowerCase().slice(2, 10)}` + : operator.address.toLowerCase().slice(2, 10) + "-new-operator"; + const operatorFilepath = path.join(keystoreDir, operatorFilename); - if (!fs.existsSync(keystoreDir)) { - fs.mkdirSync(keystoreDir, { recursive: true }); - } - fs.writeFileSync(operatorFilepath, operatorEncrypted); + await fs.promises.mkdir(keystoreDir, { recursive: true }); + await fs.promises.writeFile(operatorFilepath, operatorEncrypted); - const envPath = path.join( - __dirname, - opIndex ? `../.env.operator-${opIndex}` : "../.env.new-operator" - ); - const envContent = `# New staking provider + operator (generated by setup-new-staking-provider.js) + const envPath = path.join( + __dirname, + opIndex ? `../.env.operator-${opIndex}` : "../.env.new-operator" + ); + const envContent = `# New staking provider + operator (generated by setup-new-staking-provider.js) # Operator index: ${opIndex || "default (.env.new-operator)"} -# Add these to your shell or .env. DO NOT commit private keys. +# Source this file before running fund-new-operator.sh / run-new-operator-setup.sh. +# NEVER commit this file -- add .env.new-operator and .env.operator-* to .gitignore. # Staking provider (owns stake, authorizes, registers operator) +# WARNING: private key is NOT stored here. Save it to a hardware wallet or password manager. NEW_STAKING_PROVIDER_ADDRESS=${stakingProvider.address} -NEW_STAKING_PROVIDER_KEY=${stakingProvider.privateKey} # Operator (runs keep-client, calls joinSortitionPool) NEW_OPERATOR_ADDRESS=${operator.address} -NEW_OPERATOR_KEY=${operator.privateKey} OPERATOR_KEYSTORE_PATH=${path.resolve(operatorFilepath)} `; - fs.writeFileSync(envPath, envContent); + await fs.promises.writeFile(envPath, envContent); - console.log("=== New Staking Provider + Operator ===\n"); - console.log("Staking provider address:", stakingProvider.address); - console.log("Staking provider key: ", stakingProvider.privateKey); - console.log("\nOperator address: ", operator.address); - console.log("Operator key: ", operator.privateKey); - console.log("Operator keystore: ", operatorFilepath); - console.log("\nEnv file written to: ", path.resolve(envPath)); - console.log("\nNext steps:"); - console.log(" 1. bash scripts/fund-new-operator.sh # Transfer T + get Sepolia ETH"); - console.log(" 2. bash scripts/run-new-operator-setup.sh"); + console.log("=== New Staking Provider + Operator ===\n"); + console.log("Staking provider address:", stakingProvider.address); + console.log("Staking provider key: ", stakingProvider.privateKey); + console.log( + " ^^^ COPY AND STORE THIS KEY NOW -- it is shown once and NOT saved to any file ^^^" + ); + console.log("\nOperator address: ", operator.address); + console.log("Operator keystore: ", operatorFilepath); + console.log("\nEnv file written to: ", path.resolve(envPath)); + console.log("\nNext steps:"); + console.log( + " 1. bash scripts/fund-new-operator.sh # Transfer T + get Sepolia ETH" + ); + console.log(" 2. bash scripts/run-new-operator-setup.sh"); + } catch (e) { + console.error(e); + process.exit(1); + } } -main().catch((e) => { - console.error(e); - process.exit(1); -}); +main(); From 42ca14f8e7a9f28b0b11939931e3b5e0719d8dd8 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Tue, 14 Apr 2026 08:50:53 +0000 Subject: [PATCH 2/5] fix(scripts): pass signing keys via ETH_PRIVATE_KEY env var, not CLI args Passing --private-key as a CLI argument exposes the key in ps aux output and persists in shell history. Using ETH_PRIVATE_KEY as an inline env assignment (ETH_PRIVATE_KEY="$key" cast send ...) keeps the key out of the argv list. Introduce _sp_cast_send_ok / _op_cast_send_ok wrappers in run-new-operator-setup.sh that inject ETH_PRIVATE_KEY for the respective signer, and replace all --private-key flag usages. Update fund-new-operator.sh likewise for the deployer key. Update run-new-operator-setup.sh usage comment to reflect that NEW_STAKING_PROVIDER_KEY and NEW_OPERATOR_KEY are no longer written to .env files and must be exported by the operator. --- scripts/fund-new-operator.sh | 5 ++-- scripts/run-new-operator-setup.sh | 49 +++++++++++++++++++------------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/scripts/fund-new-operator.sh b/scripts/fund-new-operator.sh index 07f5ec03..3470cecd 100755 --- a/scripts/fund-new-operator.sh +++ b/scripts/fund-new-operator.sh @@ -41,8 +41,9 @@ fi : "${CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY:?Set CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY (deployer with T balance)}" echo "=== Transfer 80,000 T to staking provider ===" -cast send $T_TOKEN "transfer(address,uint256)" $NEW_STAKING_PROVIDER_ADDRESS $AMOUNT_80K \ - --rpc-url $CHAIN_API_URL --private-key $CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY +ETH_PRIVATE_KEY="$CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY" \ + cast send $T_TOKEN "transfer(address,uint256)" $NEW_STAKING_PROVIDER_ADDRESS $AMOUNT_80K \ + --rpc-url $CHAIN_API_URL echo "" echo "=== Sepolia ETH ===" diff --git a/scripts/run-new-operator-setup.sh b/scripts/run-new-operator-setup.sh index fece929c..4526ac53 100644 --- a/scripts/run-new-operator-setup.sh +++ b/scripts/run-new-operator-setup.sh @@ -16,7 +16,9 @@ # # Usage: # source .env -# source .env.new-operator # or export NEW_* vars manually +# source .env.new-operator # provides addresses + OPERATOR_KEYSTORE_PATH +# export NEW_STAKING_PROVIDER_KEY=0x... # key shown once by setup-new-staking-provider.js +# export NEW_OPERATOR_KEY=0x... # or use OPERATOR_KEYSTORE_PATH + password instead # bash scripts/run-new-operator-setup.sh # set -e @@ -40,15 +42,24 @@ if [ -f .env.new-operator ]; then source .env.new-operator; fi : "${CHAIN_API_URL:?Set CHAIN_API_URL}" : "${NEW_STAKING_PROVIDER_ADDRESS:?Run setup-new-staking-provider.js first}" -: "${NEW_STAKING_PROVIDER_KEY:?Run setup-new-staking-provider.js first}" +: "${NEW_STAKING_PROVIDER_KEY:?Export NEW_STAKING_PROVIDER_KEY (shown once by setup-new-staking-provider.js)}" : "${NEW_OPERATOR_ADDRESS:?Run setup-new-staking-provider.js first}" -: "${NEW_OPERATOR_KEY:?Run setup-new-staking-provider.js first}" +: "${NEW_OPERATOR_KEY:?Export NEW_OPERATOR_KEY (shown once by setup-new-staking-provider.js)}" SP="$NEW_STAKING_PROVIDER_ADDRESS" SP_KEY="$NEW_STAKING_PROVIDER_KEY" OP="$NEW_OPERATOR_ADDRESS" OP_KEY="$NEW_OPERATOR_KEY" +# Use ETH_PRIVATE_KEY env var so keys are not passed as CLI arguments +# (--private-key exposes the key in ps aux and shell history) +_sp_cast_send_ok() { + ETH_PRIVATE_KEY="$SP_KEY" cast_send_ok "$@" +} +_op_cast_send_ok() { + ETH_PRIVATE_KEY="$OP_KEY" cast_send_ok "$@" +} + cast_send_ok() { local out tx st out=$(cast send "$@" 2>&1) || { @@ -71,40 +82,40 @@ cast_send_ok() { } echo "=== Step 1: Approve TokenStaking to spend T ===" -cast_send_ok $T_TOKEN "approve(address,uint256)" $TOKEN_STAKING $AMOUNT_80K \ - --rpc-url $CHAIN_API_URL --private-key $SP_KEY +_sp_cast_send_ok $T_TOKEN "approve(address,uint256)" $TOKEN_STAKING $AMOUNT_80K \ + --rpc-url $CHAIN_API_URL echo "=== Step 2: Stake 80,000 T (stakingProvider = beneficiary = authorizer) ===" -cast_send_ok $TOKEN_STAKING "stake(address,address,address,uint96)" \ +_sp_cast_send_ok $TOKEN_STAKING "stake(address,address,address,uint96)" \ $SP $SP $SP $AMOUNT_80K \ --gas-limit "$OPERATOR_STAKE_GAS_LIMIT" \ - --rpc-url $CHAIN_API_URL --private-key $SP_KEY + --rpc-url $CHAIN_API_URL echo "=== Step 3: Authorize for RandomBeacon (40,000 T) ===" -cast_send_ok $TOKEN_STAKING "increaseAuthorization(address,address,uint96)" \ +_sp_cast_send_ok $TOKEN_STAKING "increaseAuthorization(address,address,uint96)" \ $SP $RANDOM_BEACON $AMOUNT_40K \ - --rpc-url $CHAIN_API_URL --private-key $SP_KEY + --rpc-url $CHAIN_API_URL echo "=== Step 4: Authorize for WalletRegistry (40,000 T) ===" -cast_send_ok $TOKEN_STAKING "increaseAuthorization(address,address,uint96)" \ +_sp_cast_send_ok $TOKEN_STAKING "increaseAuthorization(address,address,uint96)" \ $SP $WALLET_REGISTRY $AMOUNT_40K \ - --rpc-url $CHAIN_API_URL --private-key $SP_KEY + --rpc-url $CHAIN_API_URL echo "=== Step 5: Register operator in RandomBeacon (staking provider signs) ===" -cast_send_ok $RANDOM_BEACON "registerOperator(address)" $OP \ - --rpc-url $CHAIN_API_URL --private-key $SP_KEY +_sp_cast_send_ok $RANDOM_BEACON "registerOperator(address)" $OP \ + --rpc-url $CHAIN_API_URL echo "=== Step 6: Register operator in WalletRegistry (staking provider signs) ===" -cast_send_ok $WALLET_REGISTRY "registerOperator(address)" $OP \ - --rpc-url $CHAIN_API_URL --private-key $SP_KEY +_sp_cast_send_ok $WALLET_REGISTRY "registerOperator(address)" $OP \ + --rpc-url $CHAIN_API_URL echo "=== Step 7: Operator joins BeaconSortitionPool ===" -cast_send_ok $RANDOM_BEACON "joinSortitionPool()" \ - --rpc-url $CHAIN_API_URL --private-key $OP_KEY +_op_cast_send_ok $RANDOM_BEACON "joinSortitionPool()" \ + --rpc-url $CHAIN_API_URL echo "=== Step 8: Operator joins EcdsaSortitionPool ===" -cast_send_ok $WALLET_REGISTRY "joinSortitionPool()" \ - --rpc-url $CHAIN_API_URL --private-key $OP_KEY +_op_cast_send_ok $WALLET_REGISTRY "joinSortitionPool()" \ + --rpc-url $CHAIN_API_URL echo "" echo "=== Done. Operator $OP is registered and in both sortition pools. ===" From 2347611752c9eeabcf3b44cc654087cf73b57393 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Tue, 14 Apr 2026 08:56:46 +0000 Subject: [PATCH 3/5] fix(deploy): remove hardcoded proxy kind and deduplicate fs import - Drop kind: "transparent" from upgradeProxy options; let the OZ plugin infer the proxy type from the deployed proxy admin slot. Hardcoding the kind risks a mismatch if the original deploy defaulted differently. Add a comment with the cast storage command to verify proxy type on-chain. - Replace two inline const fs = require("fs") declarations with a single top-level import * as fs from "fs" to match TypeScript conventions and avoid the duplicate binding. --- deploy/54_upgrade_token_staking_extended.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/deploy/54_upgrade_token_staking_extended.ts b/deploy/54_upgrade_token_staking_extended.ts index a15e2386..fdae21b5 100644 --- a/deploy/54_upgrade_token_staking_extended.ts +++ b/deploy/54_upgrade_token_staking_extended.ts @@ -1,5 +1,6 @@ import { HardhatRuntimeEnvironment } from "hardhat/types" import { DeployFunction } from "hardhat-deploy/types" +import * as fs from "fs" import { ethers, upgrades } from "hardhat" @@ -29,7 +30,6 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { proxyAddress = existing.address } else { // 07_deploy_token_staking saves to TokenStaking.json in deployments dir - const fs = require("fs") const deploymentPath = `deployments/${hre.network.name}/TokenStaking.json` if (!fs.existsSync(deploymentPath)) { log("TokenStaking not deployed, skipping upgrade") @@ -47,12 +47,16 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { "ExtendedTokenStaking" ) + // 07_deploy_token_staking uses deployProxy without specifying kind; + // the OZ plugin defaults to transparent for contracts that lack upgradeTo(). + // Verify on-chain with: + // cast storage 0xb53127684a568b3173ae13b9f8a6016e243e63b4 --rpc-url $RPC + // Non-zero = transparent proxy (ProxyAdmin slot); zero = UUPS. const upgraded = await upgrades.upgradeProxy( proxyAddress, ExtendedTokenStaking, { constructorArgs: [T.address], - kind: "transparent", } ) await upgraded.deployed() @@ -66,7 +70,6 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { address: upgraded.address, abi: JSON.parse(jsonAbi as string), } - const fs = require("fs") const deploymentsDir = `deployments/${hre.network.name}` fs.writeFileSync( `${deploymentsDir}/TokenStaking.json`, From 300b1adfcde5f4957dd88454323fbb977aadba9c Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Tue, 14 Apr 2026 09:02:41 +0000 Subject: [PATCH 4/5] fix(deployments): restore deleted mainnet TokenStaking.json The file was removed in the parent branch commit without explanation. Downstream consumers relying on deployments/mainnet/TokenStaking.json break silently without it. Restored from the last known-good version (commit ab29e02). --- deployments/mainnet/TokenStaking.json | 1353 +++++++++++++++++++++++++ 1 file changed, 1353 insertions(+) create mode 100644 deployments/mainnet/TokenStaking.json diff --git a/deployments/mainnet/TokenStaking.json b/deployments/mainnet/TokenStaking.json new file mode 100644 index 00000000..79c9dd16 --- /dev/null +++ b/deployments/mainnet/TokenStaking.json @@ -0,0 +1,1353 @@ +{ + "address": "0x01B67b1194C75264d06F808A921228a95C765dd7", + "abi": [ + { + "type": "constructor", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "_token" + }, + { + "type": "address", + "name": "_nucypherVendingMachine" + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "ApplicationStatusChanged", + "inputs": [ + { + "type": "address", + "name": "application", + "indexed": true + }, + { + "type": "uint8", + "name": "newStatus", + "indexed": true + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "AuthorizationCeilingSet", + "inputs": [ + { + "type": "uint256", + "name": "ceiling", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "AuthorizationDecreaseApproved", + "inputs": [ + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "address", + "name": "application", + "indexed": true + }, + { + "type": "uint96", + "name": "fromAmount", + "indexed": false + }, + { + "type": "uint96", + "name": "toAmount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "AuthorizationDecreaseRequested", + "inputs": [ + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "address", + "name": "application", + "indexed": true + }, + { + "type": "uint96", + "name": "fromAmount", + "indexed": false + }, + { + "type": "uint96", + "name": "toAmount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "AuthorizationIncreased", + "inputs": [ + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "address", + "name": "application", + "indexed": true + }, + { + "type": "uint96", + "name": "fromAmount", + "indexed": false + }, + { + "type": "uint96", + "name": "toAmount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "AuthorizationInvoluntaryDecreased", + "inputs": [ + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "address", + "name": "application", + "indexed": true + }, + { + "type": "uint96", + "name": "fromAmount", + "indexed": false + }, + { + "type": "uint96", + "name": "toAmount", + "indexed": false + }, + { + "type": "bool", + "name": "successfulCall", + "indexed": true + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "AutoIncreaseToggled", + "inputs": [ + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "bool", + "name": "autoIncrease", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "DelegateChanged", + "inputs": [ + { + "type": "address", + "name": "delegator", + "indexed": true + }, + { + "type": "address", + "name": "fromDelegate", + "indexed": true + }, + { + "type": "address", + "name": "toDelegate", + "indexed": true + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "DelegateVotesChanged", + "inputs": [ + { + "type": "address", + "name": "delegate", + "indexed": true + }, + { + "type": "uint256", + "name": "previousBalance", + "indexed": false + }, + { + "type": "uint256", + "name": "newBalance", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "GovernanceTransferred", + "inputs": [ + { + "type": "address", + "name": "oldGovernance", + "indexed": false + }, + { + "type": "address", + "name": "newGovernance", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "MinimumStakeAmountSet", + "inputs": [ + { + "type": "uint96", + "name": "amount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "NotificationRewardPushed", + "inputs": [ + { + "type": "uint96", + "name": "reward", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "NotificationRewardSet", + "inputs": [ + { + "type": "uint96", + "name": "reward", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "NotificationRewardWithdrawn", + "inputs": [ + { + "type": "address", + "name": "recipient", + "indexed": false + }, + { + "type": "uint96", + "name": "amount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "NotifierRewarded", + "inputs": [ + { + "type": "address", + "name": "notifier", + "indexed": true + }, + { + "type": "uint256", + "name": "amount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "PanicButtonSet", + "inputs": [ + { + "type": "address", + "name": "application", + "indexed": true + }, + { + "type": "address", + "name": "panicButton", + "indexed": true + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "SlashingProcessed", + "inputs": [ + { + "type": "address", + "name": "caller", + "indexed": true + }, + { + "type": "uint256", + "name": "count", + "indexed": false + }, + { + "type": "uint256", + "name": "tAmount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "Staked", + "inputs": [ + { + "type": "uint8", + "name": "stakeType", + "indexed": true + }, + { + "type": "address", + "name": "owner", + "indexed": true + }, + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "address", + "name": "beneficiary", + "indexed": false + }, + { + "type": "address", + "name": "authorizer", + "indexed": false + }, + { + "type": "uint96", + "name": "amount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "TokensSeized", + "inputs": [ + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "uint96", + "name": "amount", + "indexed": false + }, + { + "type": "bool", + "name": "discrepancy", + "indexed": true + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "ToppedUp", + "inputs": [ + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "uint96", + "name": "amount", + "indexed": false + } + ] + }, + { + "type": "event", + "anonymous": false, + "name": "Unstaked", + "inputs": [ + { + "type": "address", + "name": "stakingProvider", + "indexed": true + }, + { + "type": "uint96", + "name": "amount", + "indexed": false + } + ] + }, + { + "type": "function", + "name": "applicationInfo", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address" + } + ], + "outputs": [ + { + "type": "uint8", + "name": "status" + }, + { + "type": "address", + "name": "panicButton" + } + ] + }, + { + "type": "function", + "name": "applications", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "uint256" + } + ], + "outputs": [ + { + "type": "address" + } + ] + }, + { + "type": "function", + "name": "approveApplication", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "application" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "approveAuthorizationDecrease", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [ + { + "type": "uint96" + } + ] + }, + { + "type": "function", + "name": "authorizationCeiling", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint256" + } + ] + }, + { + "type": "function", + "name": "authorizedStake", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "address", + "name": "application" + } + ], + "outputs": [ + { + "type": "uint96" + } + ] + }, + { + "type": "function", + "name": "checkpoints", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "account" + }, + { + "type": "uint32", + "name": "pos" + } + ], + "outputs": [ + { + "type": "tuple", + "name": "checkpoint", + "components": [ + { + "type": "uint32", + "name": "fromBlock" + }, + { + "type": "uint96", + "name": "votes" + } + ] + } + ] + }, + { + "type": "function", + "name": "delegateVoting", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "address", + "name": "delegatee" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "delegates", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "account" + } + ], + "outputs": [ + { + "type": "address" + } + ] + }, + { + "type": "function", + "name": "disableApplication", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "application" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "forceDecreaseAuthorization", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "address", + "name": "application" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "forceUnstakeLegacy", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "forceUnstakeLegacy", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address[]", + "name": "_stakingProviders" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "getApplicationsLength", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint256" + } + ] + }, + { + "type": "function", + "name": "getAutoIncreaseFlag", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "type": "function", + "name": "getAvailableToAuthorize", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "address", + "name": "application" + } + ], + "outputs": [ + { + "type": "uint96", + "name": "availableTValue" + } + ] + }, + { + "type": "function", + "name": "getMinStaked", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "uint8", + "name": "stakeTypes" + } + ], + "outputs": [ + { + "type": "uint96" + } + ] + }, + { + "type": "function", + "name": "getPastTotalSupply", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "uint256", + "name": "blockNumber" + } + ], + "outputs": [ + { + "type": "uint96" + } + ] + }, + { + "type": "function", + "name": "getPastVotes", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "account" + }, + { + "type": "uint256", + "name": "blockNumber" + } + ], + "outputs": [ + { + "type": "uint96" + } + ] + }, + { + "type": "function", + "name": "getSlashingQueueLength", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint256" + } + ] + }, + { + "type": "function", + "name": "getStartStakingTimestamp", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [ + { + "type": "uint256" + } + ] + }, + { + "type": "function", + "name": "getVotes", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "account" + } + ], + "outputs": [ + { + "type": "uint96" + } + ] + }, + { + "type": "function", + "name": "governance", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "address" + } + ] + }, + { + "type": "function", + "name": "increaseAuthorization", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "address", + "name": "application" + }, + { + "type": "uint96", + "name": "amount" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "initialize", + "constant": false, + "payable": false, + "inputs": [], + "outputs": [] + }, + { + "type": "function", + "name": "minTStakeAmount", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint96" + } + ] + }, + { + "type": "function", + "name": "notificationReward", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint256" + } + ] + }, + { + "type": "function", + "name": "notifiersTreasury", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint256" + } + ] + }, + { + "type": "function", + "name": "numCheckpoints", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "account" + } + ], + "outputs": [ + { + "type": "uint32" + } + ] + }, + { + "type": "function", + "name": "pauseApplication", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "application" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "processSlashing", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "uint256", + "name": "count" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "pushNotificationReward", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "uint96", + "name": "reward" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "requestAuthorizationDecrease", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "address", + "name": "application" + }, + { + "type": "uint96", + "name": "amount" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "requestAuthorizationDecrease", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "rolesOf", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [ + { + "type": "address", + "name": "owner" + }, + { + "type": "address", + "name": "beneficiary" + }, + { + "type": "address", + "name": "authorizer" + } + ] + }, + { + "type": "function", + "name": "seize", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "uint96", + "name": "amount" + }, + { + "type": "uint256", + "name": "rewardMultiplier" + }, + { + "type": "address", + "name": "notifier" + }, + { + "type": "address[]", + "name": "_stakingProviders" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "setAuthorizationCeiling", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "uint256", + "name": "ceiling" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "setMinimumStakeAmount", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "uint96", + "name": "amount" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "setNotificationReward", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "uint96", + "name": "reward" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "setPanicButton", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "application" + }, + { + "type": "address", + "name": "panicButton" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "slash", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "uint96", + "name": "amount" + }, + { + "type": "address[]", + "name": "_stakingProviders" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "slashingQueue", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "uint256" + } + ], + "outputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "uint96", + "name": "amount" + } + ] + }, + { + "type": "function", + "name": "slashingQueueIndex", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint256" + } + ] + }, + { + "type": "function", + "name": "stake", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "address", + "name": "beneficiary" + }, + { + "type": "address", + "name": "authorizer" + }, + { + "type": "uint96", + "name": "amount" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "stakedNu", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [ + { + "type": "uint256", + "name": "nuAmount" + } + ] + }, + { + "type": "function", + "name": "stakes", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [ + { + "type": "uint96", + "name": "tStake" + }, + { + "type": "uint96", + "name": "keepInTStake" + }, + { + "type": "uint96", + "name": "nuInTStake" + } + ] + }, + { + "type": "function", + "name": "toggleAutoAuthorizationIncrease", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "topUp", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "uint96", + "name": "amount" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "transferGovernance", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "newGuvnor" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "unstakeAll", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "unstakeKeep", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "unstakeNu", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "unstakeT", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "stakingProvider" + }, + { + "type": "uint96", + "name": "amount" + } + ], + "outputs": [] + }, + { + "type": "function", + "name": "withdrawNotificationReward", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "address", + "name": "recipient" + }, + { + "type": "uint96", + "name": "amount" + } + ], + "outputs": [] + } + ] +} \ No newline at end of file From ea8ae812c880334aee5a7d959ae58454397d2853 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Tue, 14 Apr 2026 09:02:56 +0000 Subject: [PATCH 5/5] fix(TokenStaking): add zero-address guard to increaseAuthorization approveApplication already checks application != address(0) but increaseAuthorization did not. The APPROVED status check provides a functional backstop, but adding the explicit guard makes the invariant consistent across both entry points. --- contracts/staking/TokenStaking.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/staking/TokenStaking.sol b/contracts/staking/TokenStaking.sol index 93464b28..d544affc 100644 --- a/contracts/staking/TokenStaking.sol +++ b/contracts/staking/TokenStaking.sol @@ -300,6 +300,7 @@ contract TokenStaking is Initializable, IStaking, Checkpoints { address application, uint96 amount ) external virtual override onlyAuthorizerOf(stakingProvider) { + require(application != address(0), "Parameters must be specified"); require(amount > 0, "Parameters must be specified"); ApplicationInfo storage applicationStruct = applicationInfo[ application