The one-sentence version
Once contract code is deployed on the blockchain, it cannot be modified; even if there is a bug, it cannot be fixed. Therefore, many projects use a proxy pattern: users interact with a fixed "proxy contract" address, which then forwards calls to a replaceable "implementation contract." This enables upgrades. The cost is: whoever can upgrade, can take everything. Stolen administrator private keys, incorrect storage layouts during upgrades, and uninitialized implementation contracts have all caused massive losses. Defenses rely on multi-sig, timelocks, and rigorous upgrade audits.
This article has three layers:
- Layer 1: The Intuition: Swapping the core without changing the shell
- Layer 2: Real-World Use: Parity multi-sig freeze, Audius, and stolen administrator private keys
- Layer 3: Principles and engineering key points: How proxies work, storage conflicts, initialization, and upgrade governance
Layer 1: The Intuition
1.1 Swapping the core without changing the shell
Imagine a vending machine where the shell (the address, the money stored inside, and the inventory) remains fixed, but the "brain chip" (the logic code) can be unplugged and replaced with a new one.
- Pros: Bugs can be fixed, and new features can be added without users needing to change their interaction address;
- Risks: Whoever holds the "key to swap the chip" can swap in a chip that says "dispense all the money to me."
1.2 Users are actually trusting the administrator
Many users assume "Smart Contract = Code is Law, no one can change it." But for upgradeable contracts, the reality is "Code is Law, until the administrator decides to change the code." Users are actually trusting the administrator's private key and their integrity.
Layer 2: Real-World Use
| Event | Description |
|---|---|
| Parity Multi-sig Wallet (November 2017) | Many multi-sig wallets shared a single "library contract" to provide logic. This library contract itself was not initialized; a user called the initialization function and became its owner, then subsequently (allegedly by mistake) called the self-destruct function. All wallets depending on it lost their logic code, resulting in approximately 510,000 ETH being permanently frozen. |
| Audius (2022) | A storage layout conflict occurred between the proxy contract and the implementation contract, causing the "is initialized" flag to be read incorrectly. An attacker was able to re-initialize the governance contract, set themselves as the administrator, and drain approximately $6 million worth of tokens. |
| Stolen Administrator Private Keys (Commonly seen) | The project's deployment private key or administrator private key is stolen, allowing the attacker to directly upgrade the contract to a malicious version or call management functions like "emergency withdraw" to drain funds. |
Layer 3: Principles and Engineering
3.1 How proxies work
The key is delegatecall: The proxy borrows the code from the implementation contract but executes it on the proxy's own storage. Therefore, all data is stored in the proxy, so data is not lost when switching implementation contracts. (The danger of delegatecall is discussed in "EOA vs. Smart Contract Wallets".)
3.2 Storage conflicts
Contract state variables are placed in storage slots numbered 0, 1, 2... according to their declaration order. The proxy and the implementation contract share the proxy's storage. If:
- The proxy puts the "administrator address" in slot 0;
- The implementation contract also puts "total supply" in slot 0;
Then modifying the total supply will overwrite the administrator address, and vice versa.
Countermeasures:
- Place the proxy's own variables in a random, non-conflicting slot (EIP-1967 specifies using the hash of a specific string as the slot ID);
- When upgrading, you can only append new variables at the end; you cannot delete, reorder, or change types;
- Use tools (such as the OpenZeppelin Upgrades plugin) to automatically check the storage layout compatibility between old and new versions.
3.3 Initialization
In the proxy pattern, the implementation contract's constructor does not run on the proxy's storage, so a initialize function is used instead. Common errors:
| Error | Consequence |
|---|---|
Forgot to call initialize after deployment |
Anyone can call it first and become the owner |
initialize can be called multiple times |
An attacker re-initializes and seizes control |
| The implementation contract itself is not initialized | An attacker initializes the implementation contract directly to cause damage (Parity incident) |
Countermeasures: Add a "can only be called once" protection to the initialization function; call _disableInitializers() in the implementation contract's constructor to lock it down; complete deployment and initialization within the same transaction.
3.4 Upgrade governance
| Measure | Function |
|---|---|
| Multi-sig Administrator | A single stolen private key is insufficient to perform an upgrade (see "EOA vs. Smart Contract Wallets") |
| Timelock | After an upgrade proposal is announced, there must be a waiting period (e.g., 48 hours) before it can be executed. This gives users and security researchers time to audit; those who disagree can withdraw their funds (an on-chain cooling-off period) |
| Public Upgrade Content | The source code of the new implementation contract is verified and made public on the block explorer |
| Relinquishing Upgrade Rights | Once the protocol matures, transfer administrator rights to a governance contract or renounce them entirely, making the contract truly immutable |
3.5 How users can judge
Check the contract on a block explorer:
- Is it a proxy contract (browsers usually label it "Proxy")?
- Is the administrator a regular address, a multi-sig, or a timelock contract?
- Are there recent upgrade records?
If a contract holding large amounts of user funds has an administrator that is just a regular address and has no timelock, it means a single private key decides the fate of everyone's funds.
Summary
| Layer | Key points |
|---|---|
| Intuition | Swapping the core without changing the shell; whoever can swap the core can take everything |
| Principles | Proxy uses delegatecall to borrow code but uses its own storage |
| Pitfalls | Storage conflicts (Audius), uninitialized (Parity), stolen administrator private keys |
| Defense | EIP-1967 slots, append-only variables, locked initialization, multi-sig + timelock |