Proxies and upgradeable contracts - delegatecall, UUPS and the price of flexibility
How proxy contracts work, what delegatecall does, how Transparent and UUPS differ, why EIP-1967 slots are needed and why Coin-Trust chose an immutable contract.
In this article
The code of a deployed contract cannot be changed. That is a strength (the rules cannot be swapped) and a weakness (a found bug cannot be fixed). To combine a stable address with the ability to upgrade, proxies were invented. But this flexibility is costly: it adds trust in whoever can upgrade. This article explains the mechanics and the trade-off. The basics of contracts are in the article what a smart contract is.
The idea: one address, swappable logic
Instead of one contract there are two:
- The proxy - a thin contract at a permanent address. It holds the data and can do nothing itself.
- The implementation - a contract with the code (logic). It can be replaced by a new version.
The user calls the proxy. It forwards the call to the current implementation but runs its code on its own data. To upgrade, the owner changes the implementation address in the proxy: the address and data stay the same, while the logic is new.
delegatecall: someone else's code on my data
The key operation is delegatecall. An ordinary call (call) runs another contract's code in its context. delegatecall runs someone else's code in the caller's context: the same msg.sender, the same storage, the same balance.
// simplified
fallback() external payable {
address impl = _implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch ok case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) }
}
}Any call not found in the proxy itself (fallback) is forwarded to the implementation along with the data (the call format). This works at the level of the EVM.
Where to store the implementation address
The implementation address must also live in the proxy's storage. If put in slot 0, it would collide with the implementation's variables. The EIP-1967 standard defines a "random" slot:
slot = keccak256("eip1967.proxy.implementation") - 1
= 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbcThe slot is chosen via a hash, so it does not overlap with ordinary variables. Block explorers know this slot and show "Read as Proxy".
Kinds of proxy
| Scheme | Where the upgrade logic lives | Peculiarity |
|---|---|---|
| Transparent | in the proxy | the admin cannot call the logic, others cannot administer |
| UUPS | in the implementation | cheaper per call, but a new version must contain the upgrade function |
| Beacon | at the "beacon" | one beacon upgrades many proxies at once |
| Minimal proxy (EIP-1167) | no upgrade | cheap clones of one implementation |
Initialisers instead of a constructor
A constructor runs when the implementation is deployed and writes data to its storage, not the proxy's. So upgradeable contracts use an ordinary function initialize() called once through the proxy instead.
Storage layout compatibility
A new implementation must preserve the order and types of the old variables. If version 2 inserts a new variable in the middle, all the following shift and read other people's data.
// v1
address owner; // slot 0
uint256 total; // slot 1
// v2 - a bug: a field inserted in the middle
address owner; // slot 0
uint256 fee; // slot 1 - now reads the old total!
uint256 total; // slot 2The rule: new variables only at the end (or use reserved "empty" slots). Layout checks are automated by tools.
The main risk: trust in whoever upgrades
Proxies turn "code is law" into "code, until the administrator decides otherwise". Whoever can change the implementation can, in principle, replace the logic with a malicious one and take the funds. So trust is placed not only in the code but also in governance. Measures:
- the admin is a multisig, not one key;
- a timelock: an upgrade applies after a delay and users have time to exit;
- public audits of every new version (audits);
- over time - renouncing the upgrade right or handing governance to the community.
Why Coin-Trust is immutable
The escrow contract is deployed without a proxy: its code cannot be changed by anyone, including the developers. This is a deliberate choice for a service that holds other people's money:
- users need not trust the administrator's future decisions;
- the source code is verified once and stays that way forever;
- there is no class of vulnerabilities with initialisers and storage layout.
The price of the choice: a bug cannot be fixed by an upgrade. So the contract was made as simple as possible and is covered by tests (including fuzzing), and the owner has only limited administrative rights (owner rights). A new version would mean a new contract at a new address, while deals in the old one would finish by the old rules (addresses via CREATE2).
How to recognise a proxy
- The explorer shows a "Read as Proxy" tab or an implementation address.
- The code contains
delegatecall,implementation,upgradeTo. - The contract's readable functions are tiny, while the main logic is at another address.
Before using someone else's project, find out who holds the upgrade right and whether there is a timelock.
What to read next
Find out how unique deterministic addresses are built: CREATE2. Or how large sets of data are verified: Merkle trees.
Keep reading
- 2 · Practice
Multisig and timelock - managing a contract without a single point of failure
What a multisig (m of n) and a timelock are, how wallets like Safe work, which risks they close and which they do not, and how to use them for an owner or guarantor.
4 min read - 3 · Under the hood
CREATE2: how the same contract address appears on four networks
How CREATE2 works: the address formula, salt and bytecode, and why the escrow contract has the same address on Ethereum, Base, Polygon and BNB Chain.
5 min read
Mentioned in
- Glossary - blockchain, wallets, smart contracts and escrow from A to Z
- How to read Etherscan: checking a contract, a transaction and events
- How to verify a smart contract before using it - a step-by-step guide
- Multisig and timelock - managing a contract without a single point of failure
- The EVM under the hood: stack, memory, storage, calldata and the cost of gas
- ABI: how a function call turns into bytes - selectors, calldata and events
- Storage layout and variable packing in Solidity
- Access-control patterns in smart contracts - owner, roles and deal parties
- Non-standard tokens: USDT without a return value, fee-on-transfer and blacklists
- A catalogue of smart contract vulnerabilities: 12 classic mistakes and defences
- Rollups under the hood - batches, fraud proofs, ZK, blobs and withdrawals
- Audits and bug bounties - how smart contracts are examined and how to read a report
Ready to try it?
Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.

