Say No Way to EOA
Noah Marconi
@Noah_eth- Published on
- · 5 min read
Why your deployer key should hold no authority, ever.
On March 21, 2025, an attacker took a stolen deployer key, upgraded Zoth's proxy, and drained $8.4M from the protocol. The key that deployed the protocol could still upgrade it, and had been holding that authority since launch.
I work on the security team at the Monad Foundation, where I'm a bit of a broken record about least privilege. When somebody asks for access, the first question is whether they actually need it to do their job. This post is that question pointed at deployer keys. The answer I push teams toward is to deploy through a contract, in one atomic transaction, so that the moment deployment ends, the deployer key holds no authority at all. A multi-transaction deploy and config setup cannot guarantee your desired end state, and a good chunk of this post is about why.
The usual advice
Let's peek at the standard guidance, which is reasonable as far as it goes. Foundry nudges you toward an encrypted keystore instead of a plaintext key. They also support hardware wallets. Hardhat's tutorial shows you how to use an env private key, then warns you not to.
The stock standard options look like this:
- Raw private key. No, just don't. …also sign the pledge.
- Encrypted keystore. Added safety. Needs your PW to do any harm. Still dangerous.
- Hardware wallet. Getting there but still a single point of failure.
Each option in that list raises the cost of stealing the key without limiting what the key is authorized to do. Whoever ends up holding it, you or an attacker, can do everything the deployer can do, and in Zoth's case that included upgrading the proxy.
Two incidents
The Zoth deploy transaction is one you can fetch and decode to view the initializer yourself. It sets a handful of addresses and assigns upgrade authority to the deployer. Like many deployments, deployer as admin is a convenient way to own admin locally for post-deploy configuration and normally pairs with a plan to hand authority to a multisig later. The handoff had not happened when the key was stolen. Per Halborn:
The attacker gained access to the key that controlled the deployer address for the protocol's proxy contract. With this, they were able to perform a malicious upgrade to the protocol's contracts, providing [access] to the assets that they held.
Thirteen months later, in April 2026, Wasabi lost $5M across multiple chains the same way, down to the deploy tx with its deployer as admin assigned in the initializer.
Halborn again:
The attacker used a compromised deployer key to update smart contracts and drain funds across multiple blockchains…
A hardware wallet would not eliminate risk in either case. The same key on a Ledger is harder to steal, but it still carries the authority to upgrade the whole protocol.
One message to not forget
Admins MUST be multisig.
Why not just a deploy script?
Of note is that a forge or Hardhat deploy script isn't one atomic action. They run as a sequence of independent transactions:
tx 1 deploy implementation
tx 2 deploy proxy
tx 3 initialize(...) <- front-runnable
tx 4 configure(...) <- can fail
tx 5 transferOwnership(safe) <- can fail
Tx 3 can be front-run before you initialize. See the CPIMP attack (Clandestine Proxy In The Middle of Proxy) for an illustration of how this can bite you.
Tx 4 can revert while the script keeps going, or get skipped when someone reruns half the sequence by hand at 2am after a broken broadcast. Writing the handoff into the script just schedules it as tx 5, and tx 5 can fail like any of the others.
The fix
Here's where I've settled: use a deploy contract.
Write it, test it, audit it, and put it on chain. Put all of your config steps into the same contract, then call it atomically.
Need to seed pools and set up markets? Put those steps into the deploy contract or perform them separately using a unprivileged treasury account.

A deploy contract forces discipline we should've had anyway:
- Settle on config ahead of time. No improvising at the terminal.
- Put the config in the repo. Review like any other code, and once deployed it's etched in stone.
- Verify at deploy time. In the deploy contract itself. Incorrect state should halt the deploy.
Importantly, do not assign the deployer any rights. If there's an admin, assign to a multisig in your atomic deploy transaction.
Best in Class
Uniswap took this concept and showed off with it for v4. Their deployer competition contract deploys with CREATE2, which allowed them to deterministically pre-compute the address.
They launch a contest around the concept where whoever submits the best mined address gets to be the deployer. Legend! Something not remotely possible if the deployer maintained admin rights.
The end result was a clean deploy, some attention and fanfare around the launch, and a bad ass mined address for the PoolManager.
Be like Uni, use a deploy contract.