Understanding Smart Contract Immutability and EIP-1967 Upgradeable Contracts
Smart contracts are the backbone of blockchain applications, powering everything from decentralized finance to NFTs. One of their defining features is immutability once deployed, a smart contract’s code cannot be changed. This ensures trust because everyone knows exactly how the contract will behave.
But immutability comes with a downside: if the contract has a bug or vulnerability, it can’t be fixed. Funds can be lost, functions may malfunction, and users are stuck with the original code. This creates a real challenge for developers who want both security and flexibility.
Get Ericjo’s stories in your inbox
Join Medium for free to get updates from this writer.
This is where EIP-1967 comes in. It standardizes where the proxy stores the logic contract address and admin info, preventing accidental overwrites and storage conflicts.
The Upgradeable Contract Solution
To solve this, developers use upgradeable contracts. Instead of deploying a single contract, they deploy a proxy contract that stores the contract’s data and delegates function calls to a logic contract using delegatecall. When updates are needed, a new logic contract is deployed, and the proxy points to it preserving the existing state.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/*
This is the Logic Contract (Implementation)
It contains the actual functions we want to run
*/
contract LogicV1 {
// Storage lives in the proxy
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
function getValue() public view returns (uint256) {
return value;
}
}
/*
This is a very simple Proxy Contract using EIP-1967 storage slots
It delegates calls to the logic contract
*/
contract Proxy {
// EIP-1967 implementation slot
bytes32 private constant _IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
constructor(address _logic) {
// Store the logic contract address in the EIP-1967 slot
assembly {
sstore(_IMPLEMENTATION_SLOT, _logic)
}
}
// Delegate all calls to the logic contract
fallback() external payable {
assembly {
let impl := sload(_IMPLEMENTATION_SLOT)
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(0, 0, size)
switch result
case 0 { revert(0, size) }
default { return(0, size) }
}
}
receive() external payable {}
}Key Concepts
- Delegatecall: The proxy executes code from the logic contract using its own storage and context, allowing upgrades without losing data.
- EIP-1967 storage slots: Fixed, unique slots store the implementation (logic contract address) and admin, keeping upgrades safe.
Benefits of EIP-1967
With EIP-1967:
- Safe upgrades without overwriting proxy data
- Predictable, secure upgrade process
- Reduced risk of bugs during updates
Without EIP-1967:
- Proxy storage may accidentally overwrite important data
- Upgrades are risky and can break contract functionality
Conclusion
Upgradeable contracts combined with EIP-1967 provide a powerful way to overcome the limits of immutability. Developers can fix bugs, add new features, and maintain contract state safely all while keeping the security and trust that make blockchains reliable.
By understanding these patterns, anyone building on Ethereum can create smart contracts that are both secure and flexible.









