
Smart contracts on the blockchain are designed to be immutable, ensuring the integrity of on-chain agreements. However, this immutability poses challenges when developers need to update contract logic, fix bugs, or implement security patches. Traditionally, such changes would require deploying an entirely new contract, resulting in a new address and potential disruption to existing integrations.
The proxy pattern has emerged as a popular solution to this challenge, enabling contract upgradeability while preserving the original address. This pattern involves a two-contract system: a proxy contract and an implementation contract.
The proxy contract serves as the user-facing interface and data storage, while the implementation contract contains the actual logic.
When users interact with the proxy contract, this proxy contract uses the delegatecall() function to execute the logic from the implementation contract. This approach allows the proxy to modify its own state based on the implementation’s instructions. Upgrades are facilitated by updating a specific storage slot in the proxy contract, which points to the address of the current implementation contract.
Among the various proxy patterns available, two popular approaches are the Transparent Proxy and the Universal Upgradeable Proxy Standard (UUPS). (Another one named Diamond proxy has emerged lately but has not been studied as of the time of this writing.)
These two patterns offer different trade-offs and mechanisms for managing upgrades while maintaining contract functionality and security.
Transparent Proxy
This pattern includes the upgrade functionality within the proxy contract itself.
- Separation of concerns: The upgradeable functionality is kept within the proxy contract itself, separate from the implementation logic.
- Admin roles: A designated administrator has the privilege to interact with the proxy contract for upgrades.
- User interaction: Regular users interact with the proxy contract, which then delegates calls to the implementation/logic contract.
- Admin restrictions: The administrator cannot directly interact with the implementation contract through the proxy.

Below are two solidity files to illustrate the Transparent Proxy pattern: one for the logic contract (Implementation.sol) and one for the proxy contract (TransparentProxy.sol).
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract Implementation {
uint256 public value;
address public owner;
event ValueChanged(uint256 newValue);
function initialize(address _owner) public {
require(owner == address(0), "already initialized");
owner = _owner;
}
function setValue(uint256 _newValue) public {
require(msg.sender == owner, "Not authorized ! ");
value = _newValue;
emit ValueChanged(_newValue);
}
function getValue() public view returns (uint256) {
return value;
}
}//SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract TransparentProxy {
bytes32 private constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')));
bytes32 private constant ADMIN_SLOT = bytes32(uint256(keccak256('eip1967.proxy.admin')));
event Upgraded(address indexed implementation);
event AdminChanged(address previousAdmin, address newAdmin);
constructor(address _implementation, address _admin) {
_setImplementation(_implementation);
_setAdmin(_admin);
}
modifier ifAdmin () {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
function upgradeTo(address _newImplementation) external ifAdmin {
_setImplementation(_newImplementation);
emit Upgraded(_newImplementation);
}
function changeAdmin(address _newAdmin) external ifAdmin {
emit AdminChanged(_getAdmin(), _newAdmin);
_setAdmin(_newAdmin);
}
fallback() external {
_fallback();
}
function _fallback() private {
address _impl = _getImplementation();
require(_impl != address(0), "Implementation not set");
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
function _setImplementation(address _newImpl) private {
require(_newImpl != address(0), "Invalid implementation address");
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
sstore(slot, _newImpl)
}
}
function _setAdmin(address _newAdmin) private {
require(_newAdmin != address(0), "Invalid admin address");
bytes32 slot = ADMIN_SLOT;
assembly {
sstore(slot, _newAdmin)
}
}
function _getImplementation() private view returns (address impl) {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
impl := sload(slot)
}
}
function _getAdmin() private view returns (address adm) {
bytes32 slot = ADMIN_SLOT;
assembly {
adm := sload(slot)
}
}
}EIP-1967 (Ethereum Improvement Proposal 1967) is a standard for proxy storage slots. It's crucial for implementing upgradeable contracts securely.
Key Points of EIP-1967:
- Purpose: Standardizes storage slots for proxy contracts. Aims to prevent storage collisions between proxy and implementation contracts.
- Defined Storage Slots: Implementation Address, Admin Address, Beacon Address.
- Slot Calculation: Uses a specific formula to generate unique storage slots. Example:
bytes32(uint256(keccak256('eip1967.proxy.implementation'))) - Benefits: Improves interoperability, reduces risk of storage collisions, enables easier verification and interaction with proxy contracts.
- Usage: Widely adopted in popular libraries like OpenZeppelin. Used in our TransparentProxy example for storing implementation and admin addresses.
- Security: By using these standardized slots, it's much harder for upgrades to accidentally overwrite important proxy data.
- Transparency: Makes it easier for external tools and block explorers to recognize and interact with proxy contracts.
In practice, when implementing a proxy contract following EIP-1967, you use these standardized storage slots to store critical proxy-related data. This approach significantly enhances the security and reliability of upgradeable contract systems. The standard is a crucial part of best practices in developing upgradeable smart contracts in the Ethereum ecosystem.
Step by step process for deployment of Upgradeable smart contracts
- Deploy the Implementation contract first – this contract contains the actual logic but is not interacted with by the regular users of your dApp.
- Deploy the Proxy contract second – provide the address of the implementation contract and the admin address (often a multi-sig wallet).
- Once both contracts are deployed, the admin address needs to call the
initializefunction on the proxy contract (via block explorer UI or Foundry cast call).
This sets up the initialization for the state of your contract.
All these deployment steps can be done via a solidity scripting file (DeployScript.sol) in Foundry as such:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Script, console} from "../lib/forge-std/src/Script.sol";
import { Implementation } from "../src/Implementation.sol";
import { TransparentProxy } from "../src/TransparentProxy.sol";
import { Vm } from "../lib/forge-std/src/Vm.sol";
contract DeployScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployerAddress = vm.addr(deployerPrivateKey);
vm.startBroadcast(deployerPrivateKey);
Implementation implementation = new Implementation();
console.log("Implementation deployed at:", address(implementation));
TransparentProxy proxy = new TransparentProxy(address(implementation), deployerAddress);
console.log("TransparentProxy deployed at:", address(proxy));
}
}Then, if you have all your credentials and API keys in an environment variable file like a .envrc, after compilation, you can deploy with:
forge script \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--verify script/DeployScript.s.sol/DeployScript \
--etherscan-api-key $ETHERSCAN_API_KEY \
--chain-id <CHAIN_ID> \
--vvv
Nevertheless, most Solidity coders often choose to use OpenZeppelin libraries such as contracts-upgradeable to abstract away most of the Assembly coding for storage slots, for better security and code readability.
Universal Upgradeable Proxy Standard (UUPS)
In the UUPS pattern, upgrade logic lives in the implementation contract, keeping the proxy minimal.
- Lean proxy: Only stores implementation slot and delegates calls.
- Implementation upgrades: Calls an upgrade function on the logic contract itself.
- Better gas efficiency: Proxy operations are lighter on each call.
- Standardized: Aligns with EIP-1822 and widely supported by OpenZeppelin.
This approach is favored for its simplicity and lower runtime cost in many production setups.