From 87808a31f49eb2817e26acbc1ce999e6f4eedcee Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 28 Oct 2026 22:13:51 +0000 Subject: [PATCH] sec: deployer key from the logicsrc keyring or a prompt, never env/.env/argv Co-Authored-By: Claude Opus 5.5 --- .env.example | 5 +- hardhat.config.js | 22 +++++++-- scripts/check-address.js | 12 ++--- scripts/deploy-phantom.js | 8 +-- scripts/lib/deployer-key.js | 97 +++++++++++++++++++++++++++++++++++++ scripts/prefund-contract.js | 11 ++--- scripts/setup-deployment.js | 3 +- test/deployer-key.test.js | 23 +++++++++ 8 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 scripts/lib/deployer-key.js create mode 100644 test/deployer-key.test.js diff --git a/.env.example b/.env.example index bfe6881..e471ac0 100644 --- a/.env.example +++ b/.env.example @@ -49,7 +49,8 @@ VITE_RPC_URL=https://sepolia.infura.io/v3/your-infura-project-id # VITE_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your-alchemy-key ETHERSCAN_API_KEY=your-etherscan-api-key -PRIVATE_KEY=0x1234567890123456789012345678901234567890123456789012345678901234 +# The deployer key is never an env var: DEPLOYER_KEYRING=, or type it at the prompt. +DEPLOYER_KEYRING= # Network configuration VITE_NETWORK_NAME=Sepolia Testnet VITE_CHAIN_ID=11155111 @@ -128,7 +129,7 @@ DEBUG=true # HARDHAT DEPLOYMENT CONFIGURATION # ============================================================================= # Private key for contract deployment (NEVER commit this!) -# PRIVATE_KEY=your-wallet-private-key-for-deployment +# (deployer key: see DEPLOYER_KEYRING above; PRIVATE_KEY is refused) # Etherscan API key for contract verification # ETHERSCAN_API_KEY=your-etherscan-api-key diff --git a/hardhat.config.js b/hardhat.config.js index dc15aad..f931dd6 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,10 +1,10 @@ import '@nomicfoundation/hardhat-toolbox'; import 'dotenv/config'; +import { accountsFor } from './scripts/lib/deployer-key.js'; const { MAINNET_RPC_URL, SEPOLIA_RPC_URL, - PRIVATE_KEY, ETHERSCAN_API_KEY, BASESCAN_API_KEY, ARBISCAN_API_KEY, @@ -77,22 +77,34 @@ const config = { }, sepolia: { url: resolvedSepoliaUrl, - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + // The deployer key is asked for (keyring or prompt) only when this network is used; never env. + get accounts() { + return accountsFor('sepolia'); + }, chainId: 11155111, }, mainnet: { url: resolvedMainnetUrl, - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + // The deployer key is asked for (keyring or prompt) only when this network is used; never env. + get accounts() { + return accountsFor('mainnet'); + }, chainId: 1, }, base: { url: 'https://mainnet.base.org', - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + // The deployer key is asked for (keyring or prompt) only when this network is used; never env. + get accounts() { + return accountsFor('base'); + }, chainId: 8453, }, arbitrum: { url: 'https://arb1.arbitrum.io/rpc', - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + // The deployer key is asked for (keyring or prompt) only when this network is used; never env. + get accounts() { + return accountsFor('arbitrum'); + }, chainId: 42161, }, }, diff --git a/scripts/check-address.js b/scripts/check-address.js index d0a107a..0452f1f 100644 --- a/scripts/check-address.js +++ b/scripts/check-address.js @@ -1,18 +1,14 @@ import { ethers } from 'ethers'; import 'dotenv/config'; +import { deployerKey } from './lib/deployer-key.js'; async function checkAddresses() { console.log('🔍 Checking deployment addresses...\n'); - // Check deployer address (from PRIVATE_KEY) - if (!process.env.PRIVATE_KEY) { - console.log('❌ No PRIVATE_KEY found in .env file'); - return; - } - + // The deployer key comes from the logicsrc keyring or a prompt, never env. try { - const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); - console.log('👤 Deployer Address (from PRIVATE_KEY):'); + const wallet = new ethers.Wallet(deployerKey()); + console.log('👤 Deployer Address:'); console.log(` ${wallet.address}`); console.log(' → This address deploys the contract and pays gas fees\n'); diff --git a/scripts/deploy-phantom.js b/scripts/deploy-phantom.js index 6c856e2..8ce3f3d 100644 --- a/scripts/deploy-phantom.js +++ b/scripts/deploy-phantom.js @@ -84,8 +84,9 @@ function loadContractConfig() { function validateEnvironment() { log.step('Validating environment configuration...'); + // The deployer key is not an env var any more (logicsrc keyring or a prompt, + // see scripts/lib/deployer-key.js); hardhat asks for it for --network sepolia. const required = [ - 'PRIVATE_KEY', 'SEPOLIA_RPC_URL' ]; @@ -98,9 +99,8 @@ function validateEnvironment() { process.exit(1); } - // Validate private key format - if (!process.env.PRIVATE_KEY.startsWith('0x')) { - log.error('PRIVATE_KEY must start with 0x'); + if (process.env.PRIVATE_KEY) { + log.error('PRIVATE_KEY is set in the environment; it is no longer read. Remove it (use DEPLOYER_KEYRING or the prompt).'); process.exit(1); } diff --git a/scripts/lib/deployer-key.js b/scripts/lib/deployer-key.js new file mode 100644 index 0000000..26468e4 --- /dev/null +++ b/scripts/lib/deployer-key.js @@ -0,0 +1,97 @@ +/** + * The deployer wallet key, for the moment a script signs and no longer. + * + * Fleet rule (2026-10-28, after the dev2 wallet drain): a private key is never + * read from the environment, a .env file or a command-line argument. It comes + * from, in order: + * + * 1. the logicsrc keyring, when DEPLOYER_KEYRING names an entry + * (`logicsrc keyring add --name --type generic`): + * `logicsrc keyring restore --print-private`, which asks for the + * keyring passphrase on the terminal; + * 2. a hidden prompt on the terminal. + * + * A PRIVATE_KEY still in the environment is refused, so a stale .env cannot + * quietly keep working. Nothing here writes the key anywhere. + */ +import { execFileSync } from 'node:child_process'; +import { closeSync, openSync, readSync, writeSync } from 'node:fs'; + +let cached = null; + +function normalize(raw) { + const text = String(raw ?? '').trim(); + const m = /(?:0x)?([0-9a-fA-F]{64})/.exec(text); + if (!m) throw new Error('That is not a 32-byte hex private key.'); + return `0x${m[1]}`; +} + +function fromKeyring(entry) { + const out = execFileSync('logicsrc', ['keyring', 'restore', entry, '--print-private'], { + stdio: ['inherit', 'pipe', 'inherit'], + encoding: 'utf8', + }); + return normalize(out); +} + +function fromPrompt() { + let fd; + try { + fd = openSync('/dev/tty', 'r+'); + } catch { + throw new Error('No terminal to ask for the deployer key on. Set DEPLOYER_KEYRING to a logicsrc keyring entry.'); + } + try { + writeSync(fd, 'Deployer private key (hidden): '); + try { + execFileSync('stty', ['-echo'], { stdio: [fd, 'ignore', 'ignore'] }); + } catch { + // no stty: the key will echo, but it still never touches env or disk + } + let line = ''; + const buf = Buffer.alloc(1); + while (readSync(fd, buf, 0, 1, null) === 1) { + const ch = buf.toString('utf8'); + if (ch === '\n' || ch === '\r') break; + line += ch; + } + try { + execFileSync('stty', ['echo'], { stdio: [fd, 'ignore', 'ignore'] }); + } catch { + // ignore + } + writeSync(fd, '\n'); + return normalize(line); + } finally { + closeSync(fd); + } +} + +/** Resolve the deployer key once per process (keyring, else prompt). Never from env. */ +export function deployerKey() { + if (cached) return cached; + if (process.env.PRIVATE_KEY) { + throw new Error( + 'PRIVATE_KEY is set in the environment (or .env). It is no longer read: remove it, and store the key with ' + + '`logicsrc keyring add --name --type generic` then set DEPLOYER_KEYRING=, or type it at the prompt.', + ); + } + const entry = process.env.DEPLOYER_KEYRING; + cached = entry ? fromKeyring(entry) : fromPrompt(); + return cached; +} + +/** Networks that sign with the deployer key; everything else (hardhat, localhost) never asks. */ +export const LIVE_NETWORKS = ['sepolia', 'mainnet', 'base', 'arbitrum']; + +/** The network this hardhat process was started for. */ +export function selectedNetwork(argv = process.argv, env = process.env) { + if (env.HARDHAT_NETWORK) return env.HARDHAT_NETWORK; + const i = argv.indexOf('--network'); + return i >= 0 ? argv[i + 1] : undefined; +} + +/** `accounts` for a hardhat network: the key only when that network is the one being used. */ +export function accountsFor(network) { + return selectedNetwork() === network ? [deployerKey()] : []; +} diff --git a/scripts/prefund-contract.js b/scripts/prefund-contract.js index a88e0d9..c9218c5 100644 --- a/scripts/prefund-contract.js +++ b/scripts/prefund-contract.js @@ -6,6 +6,7 @@ */ import { ethers } from 'ethers'; +import { deployerKey } from './lib/deployer-key.js'; import { config } from 'dotenv'; // Load environment variables @@ -123,13 +124,9 @@ async function prefundContract(network = 'sepolia', privateKey, fundingAmount = async function main() { const args = process.argv.slice(2); const network = args[0] || 'sepolia'; - const privateKey = args[1] || process.env.PRIVATE_KEY; - const fundingAmount = args[2] || '0.01'; - - if (!privateKey) { - console.error('❌ Private key required. Provide as argument or set PRIVATE_KEY env var.'); - process.exit(1); - } + // Never from argv (process lists, shell history) or env: keyring or prompt. + const fundingAmount = args[1] || '0.01'; + const privateKey = deployerKey(); await prefundContract(network, privateKey, fundingAmount); } diff --git a/scripts/setup-deployment.js b/scripts/setup-deployment.js index 679cd9f..c901146 100644 --- a/scripts/setup-deployment.js +++ b/scripts/setup-deployment.js @@ -60,7 +60,6 @@ function checkEnvironmentFile() { // Read and check .env content const envContent = fs.readFileSync(envPath, 'utf8'); const requiredVars = [ - 'PRIVATE_KEY', 'SEPOLIA_RPC_URL', 'VITE_CONTRACT_ADDRESS' ]; @@ -120,7 +119,7 @@ function showSetupInstructions() { console.log('\n5. ⚙️ Configure .env file:'); console.log(' • Edit .env in your project root'); - console.log(' • Set PRIVATE_KEY=your_private_key_from_phantom'); + console.log(' • Store the deployer key: logicsrc keyring add --name --type generic, then DEPLOYER_KEYRING= (or type it at the prompt). Never PRIVATE_KEY in .env.'); console.log(' • Set SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/your_project_id'); console.log(' • Set ETHERSCAN_API_KEY=your_etherscan_key (optional, for verification)'); console.log(' • Configure game parameters (optional, defaults provided):'); diff --git a/test/deployer-key.test.js b/test/deployer-key.test.js new file mode 100644 index 0000000..001d9c7 --- /dev/null +++ b/test/deployer-key.test.js @@ -0,0 +1,23 @@ +import { expect } from 'chai'; +import { afterEach, describe, it } from 'mocha'; +import { accountsFor, deployerKey, selectedNetwork } from '../scripts/lib/deployer-key.js'; + +describe('deployer key (never from env)', () => { + afterEach(() => { + delete process.env.PRIVATE_KEY; + }); + + it('does not ask for a key when no live network is selected', () => { + expect(accountsFor('sepolia')).to.deep.equal([]); + }); + + it('reads the selected network from --network or HARDHAT_NETWORK', () => { + expect(selectedNetwork(['node', 'hardhat', 'run', 'x.js', '--network', 'base'], {})).to.equal('base'); + expect(selectedNetwork([], { HARDHAT_NETWORK: 'mainnet' })).to.equal('mainnet'); + }); + + it('refuses a PRIVATE_KEY left in the environment', () => { + process.env.PRIVATE_KEY = `0x${'1'.repeat(64)}`; + expect(() => deployerKey()).to.throw(/no longer read/); + }); +});