Here is the decision, stated honestly. If you deploy immutable and there's a critical bug, you cannot fix it — you redeploy and migrate every user, which is slow, expensive, and trust-shredding. If you deploy upgradeable, you've handed someone the power to replace the code governing user funds, and that power is now the single most valuable target in your system. There is no option without a downside. The teams that get burned are the ones who picked a side by default — "make it upgradeable so we can fix things" or "make it immutable because decentralization" — without pricing what they were buying.
The proxy pattern isn't a checkbox. It's an admin attack surface you are deliberately building into a contract that holds money. Treat that decision with the weight it deserves.
What upgradeability actually is
A proxy splits your contract into two pieces. The proxy holds the state and the funds and never changes. It forwards every call, via delegatecall, to a separate implementation contract that holds the logic. To upgrade, you point the proxy at a new implementation. The state stays; the code changes underneath it.
That delegatecall is the whole trick and the whole danger. The implementation's code runs in the proxy's storage context. Which means the implementation can read and write the proxy's storage, including the slot that records which implementation is active. Get the storage layout wrong and an upgrade corrupts state silently. Get the access control on the upgrade function wrong and anyone can repoint your proxy at a contract they wrote.
You are not adding a feature. You are adding a second contract whose only job is to be able to rewrite the first one, and then you're protecting it.
Transparent vs UUPS
Two patterns dominate, and the difference is where the upgrade logic lives.
Transparent proxy. The upgrade logic lives in the proxy. The proxy inspects the caller: admin calls hit the upgrade machinery, everyone else gets forwarded to the implementation. This is robust and hard to brick — you cannot accidentally ship an implementation that loses the ability to upgrade, because the upgrade logic isn't in the implementation. The cost is gas: every single call pays for the admin check, forever, on every user.
UUPS. The upgrade logic lives in the implementation instead. The proxy is thin and cheap; users don't pay for an admin check they'll never trigger. The catch is sharp: if you deploy a new implementation that omits the upgrade function, you have permanently bricked your ability to upgrade. The escape hatch is in the thing you're replacing, so a bad replacement removes the hatch.
// UUPS — the upgrade authority is in the implementation.
// Forget _authorizeUpgrade in a future version and you can never upgrade again.
contract Vault is UUPSUpgradeable, AccessControlUpgradeable {
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
function _authorizeUpgrade(address newImpl)
internal
override
onlyRole(UPGRADER_ROLE)
{}
}
UUPS is the more common modern choice — cheaper for users, smaller proxy. But it demands discipline you must enforce in process, not hope for: every implementation must carry the upgrade path forward, and your deployment checks must verify it before any upgrade goes live. Transparent costs gas but removes the foot-gun. For a high-traffic contract where gas-per-call compounds across millions of interactions, UUPS. For a contract where the team is small and the brick risk scares you more than the gas, transparent is the safer default.
The real attack surface is the upgrade key
Pick the pattern correctly and you've solved the easy part. The hard part: whoever controls the upgrade function controls the contract, completely. They can replace your logic with selfdestruct, or with a function that transfers every token to an address they own. There is no on-chain limit on what a new implementation can do. The security of your entire protocol collapses to the security of one privilege.
So a single EOA holding the upgrade role is not a design. It's a $X bug where X is your TVL, waiting for that key to leak. Two controls turn a proxy from a liability into a defensible system: a multisig and a timelock.
Multisig governance
The upgrade role goes to a multisig — a Gnosis Safe — requiring M of N signers to authorize. One compromised key no longer ends you; an attacker needs M. Set N too small and you've gained little. Set M too high and you cannot execute an emergency upgrade because half your signers are unreachable. Three-of-five with hardware-backed keys held by distinct people in distinct places is the workhorse for protocols in early-to-mid production. Five-of-nine for larger value. The number is not the point; signer independence is. Five signers whose keys all sit on laptops in one office is one compromise away from one key.
Timelocks
A timelock forces a delay between proposing an upgrade and executing it. Forty-eight hours is common. Within that window, the proposed upgrade is public and on-chain, which gives two things. It gives your own monitoring time to catch an upgrade nobody scheduled and respond. And it gives users who don't trust the new code time to exit before it takes effect.
The timelock is what makes upgradeability compatible with user trust. Without it, an upgrade is instant and silent, and "upgradeable" reads as "the team can rug at any moment." With it, every change is announced by the chain itself and there's a window to react. The timelock is the difference between a credible upgrade process and a backdoor with extra steps.
The tradeoff is honest: a timelock slows your emergency response. If you find a critical bug, the fix waits out the delay too. The standard resolution is to separate powers — a guardian role that can pause instantly (a narrow, safe-by-construction action), and the upgrade role that goes through the full timelocked multisig. Pause buys you time; the real fix goes through governance. Stop the bleeding now, change the code deliberately.
When immutable is the right call
Immutable is not the timid option. For the right contract it's the stronger one. It is correct when the code is simple enough to get right and verify completely, when the value of credible neutrality outweighs the value of patchability, or when the contract is narrow and well-trodden enough that the bug risk is genuinely low. A token with standard mechanics, a vault with a small and audited surface, a contract whose entire value proposition is "no one can change the rules" — these want immutability.
The honest framing: every proxy you add is attack surface you're choosing to carry in exchange for the ability to fix mistakes. If your contract is simple enough that you can be confident it's correct before deployment, immutability removes the single largest attack surface in your system — the upgrade key — entirely. Reach for upgradeability when complexity makes bugs likely and value-at-risk makes a forced migration catastrophic. Reach for immutable when the code is small, the logic is settled, and the strongest possible trust guarantee is the product.
What fixed looks like
You chose the pattern on its merits, not by default. If upgradeable: UUPS or transparent picked against your real gas profile and brick tolerance, with the storage layout managed by a gap-and-namespace discipline so upgrades can't collide. The upgrade role sits behind a three-of-five-or-better multisig of independent, hardware-backed signers, gated by a timelock long enough for users and your monitoring to react. A separate guardian can pause instantly without touching the upgrade path. Every queued upgrade fires an alert, cross-checked against your change calendar. If immutable: the code was reviewed adversarially and verified before deployment, because you don't get a second draft. Either way, the decision is documented, and the people relying on the contract can see exactly who can change it, after how long, and how to know when they do.
This is for you if
You're architecting a contract that will hold real value and you're deciding, before deployment, whether and how to make it upgradeable. Getting this right — pattern selection, storage layout discipline, multisig and timelock governance, the pause/upgrade separation, and the deployment checks that keep a UUPS contract from bricking itself — is part of the architecture work, typically inside a $100k+ build engagement where the contract secures meaningful funds. The cost of getting it wrong is the whole protocol.
This is not for you if you're shipping a memecoin where "upgradeable" means "the deployer can change the rules whenever they like" and that's the actual business model. That's a different kind of project, and it isn't ours.