Skip to content
HN On Hacker News ↗

Ship the parachute in your smart contract v1: why upgradeability and disaster recovery are not v2…

▲ 10 points • 2 comments • by giladha • 4w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is AI.

100 %

AI likelihood · overall

AI
0% human-written 100% AI-generated
SEGMENTS · HUMAN 0 of 1
SEGMENTS · AI 1 of 1
WORD COUNT 1,675
PEAK AI % 100% · §1
Analyzed
Oct 10
backend: pangram/v3.3
Segments scanned
1 windows
avg 1675 words each
Distribution
0 / 100%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,675 words · 1 segments analyzed

Human AI-generated
§1 AI · 100%

18 min read1 day ago--Your v1 smart contract is not done until it can survive you.One thing that always surprises me, and it happens more and more as the industry shifts to AI dev: the ease with which new teams omit upgradeability and disaster recovery features from their v1 smart contracts.To be clear, these features are usually on the roadmap, right next to the audit and the bug bounty and all the other things that will happen after launch. The omission is in v1 itself, the version that actually goes onchain, takes real deposits, and gets attacked first.It’s easy to see why this happens. A v1 contract without a proxy, without a pause switch, without an owner, is simpler. It demos better. It feels cleaner, more in the spirit of immutability. And when the code is generated rather than written (the default these days for most young teams), the recovery features are the first thing to disappear, because they are invisible in the happy path. An AI model generating a vault will give you deposits, withdrawals, and a yield strategy. It will rarely volunteer a circuit breaker. The demo works without one.The data on AI generated code is not reassuring here. Veracode’s 2025 GenAI study found that across more than 100 models, roughly 45% of AI generated code introduced a vulnerability from the OWASP Top 10. That is not a fringe tool producing bad output. That is the average of the field, on the kind of task teams now hand to an agent without a second read.And the consequences have started to show up with names and dates attached. In February 2026, Moonwell lost about $1.78M when an oracle misconfiguration valued cbETH at $1.12 instead of roughly $2,200, and the exposed position drained in about four minutes. A security researcher who looked at the faulty pull request flagged that it appeared to be co-authored by a frontier AI model. That is one researcher’s public read, not an official post-mortem finding, and whether a model actually caused the bug is not something anyone outside the team can prove. What is not in dispute is the shape of it: the code looked right, it compiled, it got merged, and the money was gone before a human made a decision. That is the environment your v1 now ships into.Speed is fine. I use AI in my own work on a daily basis. But it raises the stakes on the one decision that is almost free at deploy time and almost impossible to change later: whether your v1 can be fixed, paused, or unwound when, not if, something goes wrong.The tombstonesA few stories every founder should know before choosing an immutable v1.Parity, 2017. A user initialized the shared library contract behind Parity’s multisig wallets and then triggered its self destruct. Every wallet that depended on that library froze instantly. 513,774 ETH across 587 wallets, locked, not stolen, just permanently unreachable, which from the owners’ perspective is the same thing. A proposal to recover the funds, EIP-999, was put to the community and never adopted. The money is still frozen today. There was no pause, no upgrade path, no recovery function. There was nothing to argue about, because there was nothing anyone could do.The DAO, 2016. The DAO lost about 3.6M ETH, roughly $60M at the time, to a reentrancy bug in its split function. It had no pause and no upgrade mechanism either. The only recovery available was to split the entire Ethereum chain in two, which is how we got ETH and ETC. When your disaster recovery plan requires a hard fork of the underlying blockchain, you do not have a disaster recovery plan.Audius, 2022. This is the one worth getting right, because the usual retelling is wrong. It was not simply a forgotten storage gap. Audius used a custom upgradeable proxy whose admin address sat in storage slot 0, the same slot OpenZeppelin’s Initializable uses for its initialization flag. The collision made the init guard readable as already satisfied, the contracts could be reinitialized, and an attacker delegated a large block of governance tokens to themselves and pushed through a malicious proposal. About 18.56M AUDIO left the treasury, reported around $6M. Two prior audits had not caught the slot collision. The lesson is not “add a gap.” The lesson is that storage layout under a proxy is load bearing, and it fails silently.Kinto, 2025. An exploit drained roughly $1.55M, a modest number next to the others on this list. Kinto wound down within weeks. That is the part founders underrate: a protocol does not need a nine figure loss to die. It needs a loss it cannot absorb, explain, or recover from, arriving before it has the balance sheet or the trust to survive one. Recoverability is not only about getting funds back. It is about still having a company on the far side of a bad week.I‘ve written genesis contracts for an L1 and run protocol upgrades on a live EVM mainnet, with real value on the other side of the transaction. Genesis is v1 in its purest form: no previous version, no migration, no users to coordinate with, and whatever you bake in at block zero, the network inherits. Nobody who has operated a live protocol ships an immutable v1 on purpose. You learn exactly once what it feels like to know a mistake cannot be walked back, and you never design that feeling into a system again.The v1 checklistHere is what I would not launch without. Every item is cheap at deploy time. Most are impossible to retrofit.I highly recommend your team to either implement all of them in your v1 or have a very good reason why not to do so.1. Upgradeable from day one, UUPS, and the owner is the upgraderUse a proxy. Specifically, use UUPS (ERC-1822) unless you have a concrete reason not to.UUPS keeps the proxy to a minimal ERC-1967 shell and puts the upgrade logic in the implementation, where your access control already lives. There is no separate ProxyAdmin contract to deploy, fund, and keep track of, and no admin versus user selector ambiguity to reason about. OpenZeppelin has moved its own recommendation this way: its current docs describe UUPS as the lightweight, versatile default and note that the Transparent pattern carries its admin logic in the proxy and costs more to deploy. I am going to skip the exact gas-per-call numbers you will see quoted around this, because they depend on library versions that keep changing and the per-call difference against a current Transparent proxy is smaller than the folklore claims. The durable reasons to prefer UUPS are a smaller deploy footprint and one less contract in your trust surface.There is also one option most teams miss. Because the upgrade function lives in the implementation, a later version can remove it, and the contract becomes immutable by choice after it has earned stability, instead of immutable by accident on day one. Treat that removal as a deliberate, final act. Ship it by mistake and you can no longer upgrade the contract. That is not the same catastrophe as funds frozen, the business logic keeps running, but it is a self inflicted dead end with no reason to exist.The two rivals, briefly. Transparent proxies still work and are a defensible choice. Diamond (ERC-2535) is genuinely powerful for systems that outgrow the contract size limit, but it is a lot of machinery and audit surface, and rarely the right v1 for a small team. Beacon proxies make sense when one upgrade must hit many identical contracts at once, and that shared blast radius is also their risk.Now the part teams get wrong. In UUPS, the gate to the whole system is one function:function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}Force the upgrade authority to be the owner address. Not a separate upgrader key kept on a developer’s laptop, not an address nobody remembers assigning, not a role granted during testing and never revoked. The address that owns the contract is the address that can upgrade it: one key set to protect with a multisig and a timelock, one address whose compromise is already your worst case, so the upgrade path adds no new worst case of its own. Every additional privileged address you invent is an additional key that can be lost, leaked, or left behind by someone who quit. Put bluntly, v1 should not have two gods.Two more UUPS rules that are not optional:Never ship an upgrade that removes or breaks the upgrade function, unless that removal is the deliberate final step above. OpenZeppelin’s tooling has compatibility checks that catch most of these; keep them on.Test the upgrade path on a fork before mainnet. An upgrade mechanism that has never been exercised is a hypothesis, not a capability.2. The constructor trap: in upgradeable contracts, the constructor does not runThis one deserves its own section because it produces a specific, repeatable, silent bug, and generated code walks into it constantly.When you deploy an upgradeable contract, two different things exist: the implementation, deployed once, and the proxy, which holds the state and which users actually interact with. The constructor runs exactly once, on the implementation, against the implementation’s own storage. It never runs for the proxy. Every line of setup you put in a constructor, assigning an owner, linking a dependency, recording a configuration value, happens in storage nobody uses, and the proxy starts life with all of it unset.That is why upgradeable contracts initialize through an initialize() function guarded by the initializer modifier: it is ordinary function code, executed through the proxy, writing to the proxy’s storage.Now the part that bites even careful developers: inline variable initialization is constructor code. When you writeuint256 public feeBps = 250;the compiler places that assignment in the constructor. In a plain contract it works fine and reads as clean, idiomatic Solidity, which is exactly why models emit it so reliably, since most of their training examples are plain contracts. Behind a proxy, feeBps reads 0 until initialize() sets it, and nothing reverts, nothing warns, nothing