SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Deployment Script Uses Plaintext PRIVATE_KEY from .env Instead of Foundry Keystore

Root + Impact

Description

  • The normal behavior is that Deploy.s.sol deploys the HonkVerifier and TreasureHunt contracts by broadcasting the deployment transactions from the deployer account using vm.startBroadcast.

  • The specific issue is that the script loads the deployer’s private key via vm.envUint("PRIVATE_KEY"). This forces users to store their private key in plaintext in a .env file - a severe security anti-pattern. Private keys can never be rotated like API keys, and any exposure (AI agent reading the file, accidental git commit, etc.) permanently compromises the entire wallet.

// Root cause in the codebase with @> marks to highlight the relevant section
contract Deploy is Script {
function run() external {
@> uint256 deployerKey = vm.envUint("PRIVATE_KEY"); // Shouldn't be reading private keys from a .env file
uint256 initialFunding = vm.envOr("INITIAL_FUNDING", DEFAULT_INITIAL_FUNDING);
address deployer = vm.addr(deployerKey);
console2.log("Chain ID:", block.chainid);
console2.log("Deployer:", deployer);
vm.startBroadcast(deployerKey); // This needs to be changed as well
verifier = new HonkVerifier();
hunt = new TreasureHunt{value: initialFunding}(address(verifier));
vm.stopBroadcast();

Risk

Likelihood:

  • Developers follow the script exactly as written (and as documented in the code comments), which requires creating a .env file with PRIVATE_KEY=0x…

  • .env files are frequently accidentally committed, shared, or read by AI agents/tools that scan project files

Impact:

  • Complete and permanent compromise of the deployer wallet (private keys cannot be rotated)

  • Full loss of any funds or assets controlled by that address if the key is ever exposed

Proof of Concept

// No on-chain PoC required - the vulnerability is in the deployment script itself.
// 1. Create .env with PRIVATE_KEY=0x<your_key>
// 2. Run: forge script Deploy.s.sol --rpc-url <url> --broadcast
// Any AI agent or process with filesystem access can now read the plaintext key.

Recommended Mitigation

contract Deploy is Script {
function run() external {
- uint256 deployerKey = vm.envUint("PRIVATE_KEY");
+ // Use Foundry's encrypted keystore (cast wallet) instead of plaintext .env
+ // Run script with: forge script Deploy.s.sol --rpc-url <url> --account <keystore-name> --sender <sender-address> --broadcast
uint256 initialFunding = vm.envOr("INITIAL_FUNDING", DEFAULT_INITIAL_FUNDING);
- address deployer = vm.addr(deployerKey);
+ // Deployer address is now msg.sender when using --sender flag
console2.log("Chain ID:", block.chainid);
- console2.log("Deployer:", deployer);
+ console2.log("Deployer:", msg.sender);
- vm.startBroadcast(deployerKey);
+ vm.startBroadcast(); // No private key argument when using --account
verifier = new HonkVerifier();
hunt = new TreasureHunt{value: initialFunding}(address(verifier));
vm.stopBroadcast();

To run this script. Use:

# Secure keystore setup (recommended by Cyfrin/Foundry)
cast wallet import deployer --interactive
forge script Deploy.s.sol --rpc-url <url> --account deployer --sender deployer_address --broadcast

This fully eliminates plaintext private key storage and aligns with the exact security practices taught in Cyfrin Updraft courses.
Don't forget to update your README as well.

Note: The reason we pass --sender and --account is because --account is used for signing the transaction. Without --sender foundry will default back to the default address so startBroadcast() won't be sent from the correct address that you want.

Updates

Lead Judging Commences

s3mvl4d Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

plaintext private key stored in .env

The deployment workflow pulls a raw EOA private key directly from the environment with `vm.envUint("PRIVATE_KEY")` and then uses it in `vm.startBroadcast(deployerKey)`, meaning the signing key is handled as an unencrypted scalar rather than through a safer account abstraction or keystore-backed signer. It is vulnerable to accidental commit, shell/session leakage, workstation compromise, or exposure to any process that can read the environment, turning compromise of the deploy machine into immediate compromise of the deployer account itself.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!