├── tests └── Proxt.t.sol ├── .gitignore ├── .prettierrc.json ├── README.md └── contracts └── Proxy.sol /tests/Proxt.t.sol: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore compiled artifacts and cache files 2 | artifacts/ 3 | .cache/ 4 | node_modules/ 5 | coverage/ 6 | dist/ 7 | 8 | # Ignore Remix-specific files 9 | *.swp 10 | *.swo 11 | *.sol.pro 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.sol", 5 | "options": { 6 | "printWidth": 80, 7 | "tabWidth": 4, 8 | "useTabs": false, 9 | "singleQuote": false, 10 | "bracketSpacing": false 11 | } 12 | }, 13 | { 14 | "files": "*.yml", 15 | "options": {} 16 | }, 17 | { 18 | "files": "*.yaml", 19 | "options": {} 20 | }, 21 | { 22 | "files": "*.toml", 23 | "options": {} 24 | }, 25 | { 26 | "files": "*.json", 27 | "options": {} 28 | }, 29 | { 30 | "files": "*.js", 31 | "options": {} 32 | }, 33 | { 34 | "files": "*.ts", 35 | "options": {} 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimal Proxy Contract (EIP-1967) 2 | 3 | ## Disclaimer 4 | This repository contains a **minimal proxy contract** that implements **EIP-1967 storage slots**. 5 | 🚨 **This contract is NOT intended for production use**—it is purely for **educational and referential purposes**. 6 | 7 | --- 8 | 9 | ## Overview 10 | This repository showcases the implementation of an **upgradeable proxy contract** that follows **EIP-1967: Standard Proxy Storage Slots**. 11 | It leverages **OpenZeppelin's delegatecall approach** using **low-level assembly** for efficiency. 12 | 13 | ### Features 14 | - **Minimalistic** proxy contract structure 15 | - **EIP-1967 standard slots** for `admin` and `implementation` 16 | - **Delegatecall-based execution flow** 17 | - **Admin-controlled upgradability** 18 | - **OpenZeppelin-inspired assembly implementation** 19 | 20 | --- 21 | 22 | ## Understanding Proxies & EIP-1967 23 | 24 | ### **🔹 What is a Proxy Contract?** 25 | A **proxy contract** is a **smart contract that delegates calls** to an **implementation contract**. 26 | This allows **upgradability**, meaning the **logic can change without modifying the storage**. 27 | 28 | ### **🔹 EIP-1967: Why Use Standard Storage Slots?** 29 | - **Prevents storage collision** between proxy and implementation contracts. 30 | - **Defines a predictable location** for storing `implementation` and `admin` addresses. 31 | - **Ensures compatibility** across different upgradeable patterns. 32 | 33 | #### **EIP-1967 Storage Slots Used:** 34 | | Slot Name | Value | 35 | |--------------------|--------------------------| 36 | | `IMPLEMENTATION_SLOT` | `keccak256("eip1967.proxy.implementation") - 1` | 37 | | `ADMIN_SLOT` | `keccak256("eip1967.proxy.admin") - 1` | 38 | 39 | 🔗 **Reference:** [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967) 40 | 41 | --- 42 | 43 | ## ⚙️ How It Works 44 | 45 | ### **1️⃣ Deploy the Proxy Contract** 46 | - The **proxy contract** does not contain logic but forwards calls via `delegatecall()`. 47 | - It stores the **implementation contract address** in the EIP-1967 slot. 48 | 49 | ### **2️⃣ Upgrade the Implementation** 50 | - The **admin** can call `upgradeTo(newImplementation)`. 51 | - The proxy updates the **implementation contract address**. 52 | - **Storage remains unchanged**, but the contract logic is updated. 53 | 54 | ### **3️⃣ Calls are Forwarded via `delegatecall()`** 55 | - Any function calls go to `_delegate()`, executing them in the **implementation contract's** context. 56 | 57 | --- 58 | 59 | ## 🛠️ OpenZeppelin & Assembly Usage 60 | 61 | ### **🔹 OpenZeppelin Low-Level Assembly** 62 | This contract uses **inline assembly (`delegatecall`)** from OpenZeppelin's [Proxy.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.2/contracts/proxy/Proxy.sol). 63 | 64 | ```solidity 65 | assembly { 66 | calldatacopy(0, 0, calldatasize()) 67 | let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) 68 | returndatacopy(0, 0, returndatasize()) 69 | 70 | switch result 71 | case 0 { revert(0, returndatasize()) } 72 | default { return(0, returndatasize()) } 73 | } 74 | -------------------------------------------------------------------------------- /contracts/Proxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.11; 3 | 4 | contract FirstContract { 5 | uint256 public counter; 6 | 7 | function increment() external { 8 | counter += 1; 9 | } 10 | } 11 | 12 | contract SecondContract { 13 | uint256 public counter; 14 | 15 | function increment() external { 16 | counter += 1; 17 | } 18 | 19 | function decrement() external { 20 | counter -= 1; 21 | } 22 | } 23 | 24 | /** 25 | * @title Proxy Contract - A Minimal Proxy Contract 26 | * @notice Implements delegatecall-based proxy pattern with EIP-1967 storage slots. 27 | * @dev Uses OpenZeppelin's delegatecall approach. 28 | * @dev Reference: https://eips.ethereum.org/EIPS/eip-1967 (EIP-1967: Standard Proxy Storage Slots) 29 | * @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.2/contracts/proxy/Proxy.sol 30 | */ 31 | contract Proxy { 32 | /// @dev EIP-1967 standard storage slots for proxy contracts 33 | bytes32 private constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); 34 | bytes32 private constant ADMIN_SLOT = bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); 35 | 36 | /// @dev Custom errors 37 | error NotAuthorized(); 38 | error DelegateCallFailed(); 39 | error InvalidImplementation(); 40 | error AdminCannotBeZero(); 41 | 42 | /// @notice Initializes the proxy with the deployer as admin. 43 | constructor() { 44 | _setAdmin(msg.sender); 45 | } 46 | 47 | /// @notice Ensures that only the admin can execute certain functions. 48 | /// @dev If the caller is the admin, the function executes normally. 49 | /// @dev If the caller is NOT the admin, execution is forwarded to `_fallback()`, 50 | /// allowing users to interact with the implementation contract without being blocked. 51 | /// @dev This design ensures that admin-restricted functions remain protected, 52 | /// while users can still call delegated functions seamlessly through the proxy. 53 | modifier onlyAdmin() { 54 | if (msg.sender == _getAdmin() || msg.data.length == 0) { 55 | _; // Admin can execute the function normally 56 | } else { 57 | _fallback(); // Non-admin users are forwarded to the implementation contract 58 | } 59 | } 60 | 61 | function changeAdmin(address _admin) external onlyAdmin { 62 | _setAdmin(_admin); 63 | } 64 | 65 | /** 66 | * @notice Upgrades the proxy contract to a new implementation. 67 | * @dev Ensures only the admin can upgrade and verifies the implementation address. 68 | * @param _implementation Address of the new implementation contract. 69 | */ 70 | function upgradeTo(address _implementation) external onlyAdmin { 71 | if (_implementation.code.length == 0) { 72 | revert InvalidImplementation(); 73 | } 74 | _setImplementation(_implementation); 75 | } 76 | 77 | /** 78 | * @notice Returns the current admin address. 79 | * @return The admin address. 80 | */ 81 | function admin() external onlyAdmin returns (address) { 82 | return _getAdmin(); 83 | } 84 | 85 | /** 86 | * @notice Returns the current implementation address. 87 | * @return The implementation address. 88 | */ 89 | function implementation() external onlyAdmin returns (address) { 90 | return _getImplementation(); 91 | } 92 | 93 | /// @dev Internal function to fetch the admin from the EIP-1967 storage slot. 94 | function _getAdmin() private view returns (address) { 95 | return StorageSlot.getAddressSlot(ADMIN_SLOT).value; 96 | } 97 | 98 | /// @dev Internal function to set the admin in the EIP-1967 storage slot. 99 | function _setAdmin(address _admin) private { 100 | if (_admin == address(0)) { 101 | revert AdminCannotBeZero(); 102 | } 103 | StorageSlot.getAddressSlot(ADMIN_SLOT).value = _admin; 104 | } 105 | 106 | /// @dev Internal function to fetch the implementation address from storage. 107 | function _getImplementation() private view returns (address) { 108 | return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value; 109 | } 110 | 111 | /// @dev Internal function to update the implementation address in storage. 112 | function _setImplementation(address _implementation) private { 113 | if (_implementation.code.length == 0) { 114 | revert InvalidImplementation(); 115 | } 116 | StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = _implementation; 117 | } 118 | 119 | /** 120 | * @notice Performs a low-level delegate call to the implementation contract. 121 | * @dev Uses inline assembly for gas-efficient execution. 122 | * @param _implementation Address of the contract to delegate calls to. 123 | * @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.2/contracts/proxy/Proxy.sol 124 | */ 125 | function _delegate(address _implementation) private { 126 | assembly { 127 | calldatacopy(0, 0, calldatasize()) 128 | let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) 129 | returndatacopy(0, 0, returndatasize()) 130 | 131 | switch result 132 | case 0 { revert(0, returndatasize()) } 133 | default { return(0, returndatasize()) } 134 | } 135 | } 136 | 137 | /// @notice Internal fallback function that delegates execution. 138 | function _fallback() private { 139 | _delegate(_getImplementation()); 140 | } 141 | 142 | /** 143 | * @notice Forwards all calls to the implementation contract. 144 | * @dev Uses `_delegate()` to perform delegatecall. 145 | */ 146 | fallback() external payable { 147 | _fallback(); 148 | } 149 | 150 | /** 151 | * @notice Allows receiving Ether and forwards execution to implementation. 152 | */ 153 | receive() external payable { 154 | _fallback(); 155 | } 156 | } 157 | 158 | contract ProxyAdmin { 159 | address public owner; 160 | 161 | constructor() { 162 | owner = msg.sender; 163 | } 164 | 165 | modifier onlyOwner() { 166 | require(msg.sender == owner, "not owner"); 167 | _; 168 | } 169 | 170 | function getProxyAdmin(address proxy) external view returns (address) { 171 | (bool ok, bytes memory res) = proxy.staticcall(abi.encodeWithSelector(Proxy.admin.selector)); 172 | require(ok, "call failed"); 173 | return abi.decode(res, (address)); 174 | } 175 | 176 | function getProxyImplementation(address proxy) 177 | external 178 | view 179 | returns (address) 180 | { 181 | (bool ok, bytes memory res) = proxy.staticcall(abi.encodeWithSelector(Proxy.implementation.selector)); 182 | require(ok, "call failed"); 183 | return abi.decode(res, (address)); 184 | } 185 | 186 | function changeProxyAdmin(address payable proxy, address admin) 187 | external 188 | onlyOwner 189 | { 190 | Proxy(proxy).changeAdmin(admin); 191 | } 192 | 193 | function upgrade(address payable proxy, address implementation) 194 | external 195 | onlyOwner 196 | { 197 | Proxy(proxy).upgradeTo(implementation); 198 | } 199 | } 200 | 201 | /** 202 | * @title StorageSlot Utility Library 203 | * @notice Provides a way to access storage slots in a structured manner. 204 | * @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.2/contracts/utils/StorageSlot.sol 205 | */ 206 | library StorageSlot { 207 | struct AddressSlot { 208 | address value; 209 | } 210 | 211 | /** 212 | * @notice Returns a storage pointer for a given slot. 213 | * @param slot Storage slot key. 214 | * @return r A struct containing the address value at the slot. 215 | */ 216 | function getAddressSlot(bytes32 slot) 217 | internal 218 | pure 219 | returns (AddressSlot storage r) 220 | { 221 | assembly { 222 | r.slot := slot 223 | } 224 | } 225 | } 226 | 227 | /** 228 | * @title Test Contract for Storage Slots 229 | * @notice Allows testing slot reads/writes. 230 | * @dev Demonstrates how to read and write to a specific storage slot. 231 | */ 232 | contract TestSlot { 233 | bytes32 public constant slot = keccak256("TEST_SLOT"); 234 | 235 | /** 236 | * @notice Reads the stored address from the slot. 237 | * @return The stored address. 238 | */ 239 | function getSlot() external view returns (address) { 240 | return StorageSlot.getAddressSlot(slot).value; 241 | } 242 | 243 | /** 244 | * @notice Writes an address to the storage slot. 245 | * @param _addr Address to store. 246 | */ 247 | function writeSlot(address _addr) external { 248 | StorageSlot.getAddressSlot(slot).value = _addr; 249 | } 250 | } 251 | --------------------------------------------------------------------------------