This repository contains a simplified implementation of the EIP-1967 proxy standard for upgradeable smart contracts in Solidity. The goal is to demonstrate how proxy patterns enable contract upgradability while maintaining a consistent contract address and state.
A proxy pattern in Ethereum smart contracts separates the contract's logic from its storage:
- Proxy Contract: Stores the state and forwards calls to the implementation contract
- Implementation Contract: Contains the business logic that can be upgraded
This separation allows developers to upgrade a contract's functionality (fix bugs or add features) without changing the contract's address or losing its state.
EIP-1967 defines a standard for proxy contracts that focuses on:
- Standardized Storage Slots: Uses specific storage slots for implementation addresses to avoid storage collisions
- Transparency: Makes proxy patterns more transparent and interoperable
- Security: Reduces the risk of storage conflicts between proxy and implementation contracts
EIP-1967 defines specific storage slots for critical proxy information:
-
Implementation Address:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
- Calculated as
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
- Calculated as
-
Admin Address:
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
- Calculated as
bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
- Calculated as
-
Beacon Address (for beacon proxies):
0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
- Calculated as
bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)
- Calculated as
These slots are chosen to minimize the risk of collisions with the implementation contract's storage.
In Solidity, storage slots are assigned sequentially starting from slot 0 for state variables. This creates a risk of collision between proxy and implementation contracts if they use the same slots for different purposes.
EIP-1967 solves this by:
- Using cryptographically secure, random-looking slots derived from a namespaced hash
- Subtracting 1 from the hash to further reduce collision probability
- Ensuring these slots are extremely unlikely to be used by normal contract variables
For example, the implementation address is stored at a slot derived from keccak256('eip1967.proxy.implementation')
minus 1, which produces a value that's practically impossible to collide with normal variable storage.
User ──► Proxy Contract ─┬─► Implementation V1
│ (can be upgraded)
│
└─► Storage
(remains unchanged)
After upgrade:
User ──► Proxy Contract ─┬─X Implementation V1
│ (no longer used)
│
├─► Implementation V2
│ (new logic)
│
└─► Storage
(preserved)
The proxy uses Solidity's delegatecall
opcode to execute the implementation contract's code in the context of the proxy's storage:
- User sends a transaction to the proxy contract
- The proxy's fallback function uses
delegatecall
to forward the call to the implementation - The implementation's code executes using the proxy's storage
- Results are returned to the user
To upgrade the contract:
- Deploy a new implementation contract
- Call the proxy's upgrade function to update the implementation address
- All future calls to the proxy will use the new implementation's logic
This repository includes:
-
Proxy.sol: A simplified EIP-1967 proxy contract that:
- Stores the implementation address in the EIP-1967 standard slot
- Forwards calls to the implementation using
delegatecall
- Includes an upgrade mechanism controlled by an admin
-
LogicV1.sol: A basic implementation contract with:
- A counter state variable
- An initialize function to set the initial counter value
- Functions to increment and get the counter value
-
LogicV2.sol: An upgraded implementation with:
- All functionality from V1
- Additional feature to decrement the counter
There are several proxy patterns in Ethereum, each with its own advantages and trade-offs:
Key Differences from EIP-1967:
- Uses a different approach to avoid function selector clashes
- Admin-specific logic is in the proxy itself, not in a separate contract
- Less gas efficient for regular users due to additional checks
Key Differences from EIP-1967:
- Upgrade logic is in the implementation contract, not the proxy
- More gas efficient for users
- Requires each implementation to include upgrade logic
- Cannot be "bricked" by deploying an implementation without upgrade code
Key Differences from EIP-1967:
- Allows multiple implementation contracts (facets) simultaneously
- More complex but more flexible
- Better suited for large contracts that need to be split up
EIP-1967 is often preferred because:
- It's simpler to understand and implement than Diamond
- It's more standardized and widely adopted than custom proxy patterns
- It's more gas efficient than Transparent Proxies
- It separates admin logic from implementation logic, unlike UUPS
When using proxy patterns, be aware of these security concerns:
-
Storage Collisions: Implementation contracts must be careful not to use the same storage slots as the proxy
- EIP-1967 mitigates this by using specific storage slots derived from hashes
- Always maintain the same storage layout in upgraded implementations
-
Function Selector Clashes: Proxy and implementation functions can clash if they have the same selector
- This happens when a function in the implementation has the same 4-byte signature as a proxy function
- Can lead to proxy admin functions being inaccessible
-
Initialization: Implementation contracts should use initializer functions instead of constructors
- Constructors are executed only when the implementation is deployed, not when used through a proxy
- Always use a one-time initialization pattern with a guard against re-initialization
-
Access Control: Only authorized addresses should be able to upgrade the implementation
- A compromised admin key can lead to malicious upgrades
- Consider using multi-signature wallets or timelocks for admin functions
- Deploy the implementation contract (LogicV1.sol)
- Deploy the proxy contract (Proxy.sol), passing the implementation address and admin address
- Interact with the proxy as if it were the implementation
- Deploy the new implementation (LogicV2.sol)
- Call the proxy's upgrade function with the new implementation address
- The proxy now uses the new implementation while preserving its state
This project uses Foundry for development, testing, and deployment.
$ forge build
$ forge test
$ forge script script/DeployProxy.s.sol:DeployProxyScript --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast
LogicV1 deployed at: 0x350AECDbcaA3557bda602786b0D831655A53ec1D
Proxy deployed at: 0x844d5b937883bae08cA7CaA99Bc66258cBD2fC56
$ forge script script/DeployProxy.s.sol:UpgradeProxyScript --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast
- EIP-1967: Standard Proxy Storage Slots
- OpenZeppelin Upgrades Documentation
- Solidity Documentation
- ConsenSys Best Practices for Smart Contract Systems
# Install
git clone https://github.com/oumaoumag/ProxyContractImplementation.git
cd ProxyContractImplementation
forge install
# Build and test
forge build
forge test -v
- Compilation: Use Solidity ^0.8.13, try
forge clean
- Tests: Run
foundryup
to update Foundry - Deployment: Check RPC endpoint and account balanc
This project is licensed under the MIT License - see the LICENSE file for details.
Copyright (c) 2025 Ouma Ouma
This implementation of the EIP-1967 proxy pattern is provided as an educational resource and may be freely used in your own projects according to the terms of the MIT License.