fix: avoid passing private key by command line

This commit is contained in:
Gonza Montiel 2026-02-10 00:05:40 -03:00
parent 396715ecd9
commit 3a554e21c7
3 changed files with 21 additions and 14 deletions

View file

@ -206,9 +206,9 @@ const deployServiceManagerImplementation = async (
const actualDeployments = await parseDeploymentsFile(chain);
// Use environment variables to avoid command injection
// Note: Private key is passed via environment variable as required by forge
// This is a known limitation of the forge toolchain
// Use environment variables to avoid command injection and process list exposure
// Note: Private key is passed via PRIVATE_KEY environment variable (not command-line)
// to prevent it from appearing in system process lists (security best practice)
const env = {
...process.env,
PRIVATE_KEY: privateKey,
@ -226,8 +226,6 @@ const deployServiceManagerImplementation = async (
"deployServiceManagerImpl()",
"--rpc-url",
rpcUrl,
"--private-key",
privateKey,
"--broadcast",
"--non-interactive"
];
@ -296,8 +294,8 @@ const updateServiceManagerProxyWithVersion = async (
) => {
logger.info(`🔄 Updating ServiceManager proxy and setting version to ${version}...`);
// Note: Private key is passed via environment variable as required by forge
// This is a known limitation of the forge toolchain
// Note: Private key is passed via PRIVATE_KEY environment variable (not command-line)
// to prevent it from appearing in system process lists (security best practice)
const proxyAdmin = (deployments as any).ProxyAdmin ?? process.env.PROXY_ADMIN;
if (!proxyAdmin) {
throw new Error(
@ -322,8 +320,6 @@ const updateServiceManagerProxyWithVersion = async (
"updateServiceManagerProxyWithVersion()",
"--rpc-url",
rpcUrl,
"--private-key",
privateKey,
"--broadcast",
"--non-interactive"
];

View file

@ -160,10 +160,13 @@ export const fundValidators = async (options: FundValidatorsOptions): Promise<bo
const ethTransferAmount = BigInt(creatorEthBalance) / BigInt(100); // 1% of the balance
logger.debug(`Transferring ${erc20TransferAmount} tokens to each validator`);
// Security Note: Private keys are passed via stdin with --interactive flag
// using printf (not echo) to avoid command-line exposure in process lists
for (const validator of validators) {
if (validator.publicKey !== tokenCreator) {
const transferCmd = `${castExecutable} send --private-key ${creatorPrivateKey} ${underlyingTokenAddress} "transfer(address,uint256)" ${validator.publicKey} ${erc20TransferAmount} --rpc-url ${rpcUrl}`;
const transferCmd = `printf '%s\\n' "\${PRIVATE_KEY}" | ${castExecutable} send --interactive ${underlyingTokenAddress} "transfer(address,uint256)" ${validator.publicKey} ${erc20TransferAmount} --rpc-url ${rpcUrl}`;
const { exitCode: transferExitCode, stderr: transferStderr } = await $`sh -c ${transferCmd}`
.env({ ...process.env, PRIVATE_KEY: creatorPrivateKey })
.nothrow()
.quiet();
if (transferExitCode !== 0) {
@ -196,9 +199,12 @@ export const fundValidators = async (options: FundValidatorsOptions): Promise<bo
// Transfer ETH only if the validator has no ETH
if (BigInt(validatorEthBalance) === BigInt(0)) {
const ethTransferCmd = `${castExecutable} send --private-key ${creatorPrivateKey} ${validator.publicKey} --value ${ethTransferAmount} --rpc-url ${rpcUrl}`;
const ethTransferCmd = `printf '%s\\n' "\${PRIVATE_KEY}" | ${castExecutable} send --interactive ${validator.publicKey} --value ${ethTransferAmount} --rpc-url ${rpcUrl}`;
const { exitCode: ethTransferExitCode, stderr: ethTransferStderr } =
await $`sh -c ${ethTransferCmd}`.nothrow().quiet();
await $`sh -c ${ethTransferCmd}`
.env({ ...process.env, PRIVATE_KEY: creatorPrivateKey })
.nothrow()
.quiet();
if (ethTransferExitCode !== 0) {
logger.error(
`Failed to transfer ETH to validator ${validator.publicKey}: ${ethTransferStderr.toString()}`

View file

@ -45,16 +45,21 @@ export const updateValidatorSet = async (options: UpdateValidatorSetOptions): Pr
const serviceManagerAddress = deployments.ServiceManager;
invariant(serviceManagerAddress, "ServiceManager address not found in deployments");
// Security Note: Private key is passed via stdin with --interactive flag
// using printf to avoid command-line exposure in process lists (security best practice)
// Using cast to send the transaction
const executionFee = "100000000000000000"; // 0.1 ETH
const relayerFee = "200000000000000000"; // 0.2 ETH
const value = "300000000000000000"; // 0.3 ETH (sum of fees)
const sendCommand = `${castExecutable} send --private-key ${ownerPrivateKey} --value ${value} ${serviceManagerAddress} "sendNewValidatorSet(uint128,uint128)" ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
const sendCommand = `printf '%s\\n' "\${PRIVATE_KEY}" | ${castExecutable} send --interactive --value ${value} ${serviceManagerAddress} "sendNewValidatorSet(uint128,uint128)" ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
logger.debug(`Running command: ${sendCommand}`);
const { exitCode, stderr } = await $`sh -c ${sendCommand}`.nothrow().quiet();
const { exitCode, stderr } = await $`sh -c ${sendCommand}`
.env({ ...process.env, PRIVATE_KEY: ownerPrivateKey })
.nothrow()
.quiet();
if (exitCode !== 0) {
logger.error(`Failed to send validator set: ${stderr.toString()}`);