← All writing
Web3 June 13, 2026

UUPS Proxies in Practice: Upgradeability Is a Governance Problem

Web3 Solidity Smart Contracts

A few thousand people had each paid real money for a single NFT. Then we multiplied every holder’s balance by a hundred and cut the price to match. Nobody lost a dollar, and every holder ended up with exactly the value they started with.

That is upgradeability in practice, and it is a lot more delicate than it sounds.

At Ex Populus I wrote smart-contract upgrades for an epic called Tiny Keys. Here is the backstory. XAI is a layer-3 network, and the right to help secure it was sold as an NFT, a “license key.” The keys went out on a rising price curve. Every so many sales, the price stepped up. That works great right up until it doesn’t. By the time a key cost $4,000 or $5,000, people stopped buying. Too expensive. The whole distribution stalled.

The fix was essentially a stock split, for NFTs. Everyone who already held a key got airdropped ninety-nine more for free, and the price was divided by a hundred. A $5,000 key became a $50 key, and the person who had paid $5,000 now held a hundred of them. Same value in their wallet, suddenly accessible to the next buyer.

Simple idea. Now do it on contracts that are already live, holding assets people paid thousands for, without breaking a single one. That is the job. And it is why upgradeability, done seriously, is one of the hardest skills in this space.

You can’t just redeploy

Here is the mental model I use. The contract is a forwarding address. Everyone has it, everyone sends their mail there, and it quietly forwards everything to wherever you actually live. Upgrading is just changing where it forwards to. You move, the mail starts arriving at your new place, and nobody has to update their address book, because the forwarding address itself never changed.

That is what a proxy gives you. People own keys at the forwarding address. It is a promise. You cannot deploy a fresh contract somewhere else and tell everyone their old keys are worthless. You have to change where the logic lives without changing the address anyone holds.

UUPS, the Universal Upgradeable Proxy Standard (EIP-1822), does this by putting the upgrade logic inside the implementation instead of the proxy. The proxy stays tiny and cheap. But it also means the thing that lets you upgrade lives in the exact part you are swapping out. Which leads straight to the first way people blow themselves up.

Guard the upgrade, or lose everything

In UUPS, the function that performs the upgrade lives on the implementation. If you don’t guard it, anyone can call it and point your contract at logic they wrote. That is not a bug. That is a total loss.

OpenZeppelin’s UUPSUpgradeable base handles the plumbing and hands you exactly one function to override:

function _authorizeUpgrade(address newImplementation)
    internal
    override
    onlyGovernance
{}

That empty body is the most important code in the contract. Whatever you put on that modifier line, whether an owner, a multisig, or a governor, is your entire answer to “who is allowed to change the rules.” Get it wrong and nothing else you wrote matters.

Don’t rearrange the drawers

The second way people blow themselves up is storage. A split like Tiny Keys is exactly the kind of upgrade where it bites, because you are touching every holder’s balance at once.

Solidity numbers your state variables like drawers in a filing cabinet: drawer 0, drawer 1, drawer 2, and so on. The proxy owns the cabinet. The implementation is just the index that says which drawer holds what. Every version of that index has to agree with every previous version.

Slip a new drawer into the middle and everything after it shifts down by one. Now the index points at the wrong drawer, and the contract reads a balance out of what used to hold a timestamp. Live assets, corrupted in place.

The fix is boring, and you do it on day one: leave empty drawers at the end.

uint256[50] private __gap;

Reserve a block of unused slots at the end of each upgradeable contract so future versions have somewhere to add state without disturbing anything that already exists. You shrink the gap as you grow into it. It costs you nothing now and saves you from a mistake you cannot take back.

The part nobody designs early enough: governance

Everything above is table stakes. Here is the piece I think is genuinely underrated, and where I have watched serious teams get sloppy. If your protocol is actually decentralized, you are not the one clicking upgrade. Governance is.

That changes the problem. A governor contract doesn’t just need permission to upgrade. It needs to know, precisely, which functions are allowed to be called and with what parameters. And functions on-chain aren’t names. They are selectors: the first four bytes of the keccak hash of the signature. upgradeTo(address) is a specific four bytes. setPrice(uint256) is another four bytes entirely.

So you have to think through your whole surface area in advance. Every function signature, every parameter, what the governor may and may not authorize, all of it before you ship. Because once the protocol is live and the keys are in the hands of token holders, you don’t get a quiet do-over. You can’t slip in a function you forgot to account for. In a lot of cases, designing that permission set after the fact simply isn’t possible.

Here is the part that doesn’t get said out loud. A lot of protocols dodge all of this by keeping a back door: a guardian council, an admin multisig, an upgrade key the team quietly holds. Something that can reach in and fix things when they need to. That solves the no-second-chances problem. It also means the protocol was never really decentralized. If a small group can change the rules, then the thing users are trusting isn’t the code. It is those people, and their promise not to abuse the key.

Maybe that is an acceptable trade for what you are building. Just be honest about which one it is. But if you want people to trust the protocol more than they trust your team, you don’t get the escape hatch. Which is exactly why the design has to be right the first time.

Upgradeability and governance are the same skill. People treat the upgrade mechanism as a feature and governance as DAO tooling you bolt on at the end. In practice they are one design decision, and on a protocol that holds real value, it is one of the most important ones you will make.

Prove the upgrade before you ship it

Last piece: test the upgrade itself, not just the contracts.

With Foundry you can fork mainnet state, take the actual contract at its real address, simulate the upgrade, and assert that every invariant holds before and after. Balances move exactly the way you intended, and nothing else moves. When real holders are on the other side of the change, a flawed upgrade isn’t a bug ticket. It is not recoverable. So you earn your confidence against the real path, not a fresh deploy in a clean room.

The takeaway

Upgradeability looks like convenience. “We can fix it later.” It is actually a commitment you make on day one: to a fixed address, to a frozen storage layout, and to a governance model that has to be designed before anyone trusts you with a dollar. Treat it that way and a proxy is one of the most powerful tools you have. Treat it as a feature you bolt on, and it is the most expensive line in the contract.

← All writing