A Solidity library for validating proxy contract upgrades with runtime integrity checks supporting various proxy patterns.
- Overview
- Main Features
- Installation
- Project Structure
- Key Components
- Quick Start
- License
- Contributing
- Support
Upgrade Checker is a production-ready Solidity library that provides robust, onchain validation mechanisms for proxy contract upgrades. It prevents upgrading the proxy to invalid implementations by enforcing strict contract verification rules at upgrade time.
- Beacon Proxy - Beacon-controlled upgrades
- Transparent Upgradeable Proxy - Proxy-controlled upgrades
- UUPS Proxy - Implementation-controlled upgrades
- Contract Name Matching - Verify implementation identity via unique
contractName() - ERC165 Interface Checking - Required interfaceIds validation (optional)
- ERC7201 Namespaced Storage - Avoid storage collisions
- Dynamic Configuration - Add/remove interfaceIds requirements at runtime
- Interface Check Toggle - Enable/disable validation as needed
- Copy-paste ready examples for each upgradeable proxy pattern (Diamond TBA)
- Comprehensive test suite with 50+ test cases
- Clear error messages and documentation
- Node.js 18.0.0 or later
- npm or yarn package manager
- Git for version control
git clone https://github.com/RevxChain/upgrade-checker.git
cd upgrade-checkernpm installThis installs:
- Hardhat - Ethereum development framework
- OpenZeppelin Contracts - Audited smart contract library
- Hardhat Plugins - Testing, upgrades, gas reporting, sizing
cp .env.example .envEdit .env with your configuration:
# PRIVATE KEYS
PRIVATE_KEY = your_private_key_here
# MAINNET FORK TEST
FORK_RPC_URL = https://eth.llamarpc.com
FORK_BLOCK_NUMBER = 24540000
# RPC URLS
ETH_RPC_URL = https://eth.llamarpc.com
BSC_RPC_URL = https://binance.llamarpc.com
ARBITRUM_RPC_URL = https://arbitrum.llamarpc.com
POLYGON_RPC_URL = https://polygon.llamarpc.com
BASE_RPC_URL = https://base.llamarpc.com
npx hardhat compileCompiles all Solidity contracts and generates artifacts.
npx hardhat testnpx hardhat coverageEdit .env with your configuration:
# MAINNET FORK TEST
FORK_RPC_URL = https://eth.llamarpc.com
FORK_BLOCK_NUMBER = 24540000Set mainnet forking enabled in the hardhat.config.js:
hardhat: {
allowUnlimitedContractSize: false,
blockGasLimit: 16777216,
forking: {
url: process.env.FORK_RPC_URL !== undefined ? process.env.FORK_RPC_URL : "https://eth.llamarpc.com",
blockNumber: process.env.FORK_BLOCK_NUMBER !== undefined ? process.env.FORK_BLOCK_NUMBER : 24540000,
enabled: true
}
},
Launch a local fork of the network in the first CLI:
npx hardhat nodeRun tests in the second CLI:
npx hardhat test --network localhostnpm install @revxchain/upgrade-checkerThe package requires OpenZeppelin libraries as peer dependencies.
Install, if it is missing:
npm install @openzeppelin/contracts@5.6.xnpm install @openzeppelin/contracts-upgradeable@5.6.xupgrade-checker/
├── contracts/ # Smart contract source code
│ │
│ ├── UpgradeChecker.sol # Core abstract checker (proxy side)
│ ├── UpgradeCheckerImplementation.sol # Core abstract implementation (implementation side)
│ ├── interfaces/
│ │ └── IUpgradeChecker.sol # Interface specification
│ ├── libraries/
│ │ └── InterfaceIdsRegistry.sol # ERC165 interfaceIds registry (proxy side)
│ ├── beacon/
│ │ ├── BeaconUpgradeChecker.sol # Beacon checker (proxy side)
│ │ └── BeaconUpgradeCheckerExample.sol # Beacon example
│ ├── transparent/
│ │ ├── TransparentUpgradeChecker.sol # Transparent checker (proxy side)
│ │ └── TransparentUpgradeCheckerExample.sol # Transparent example
│ ├── uups/
│ │ ├── UUPSUpgradeChecker.sol # UUPS checker (combined)
│ │ └── UUPSUpgradeCheckerExample.sol # UUPS example
│ ├── proxies/
│ │ └── Proxies.sol # Proxy imports
│ └── mocks/
│ ├── InvalidImplementations.sol # Test implementations
│ ├── InterfaceIdsRegistryMock.sol # InterfaceIdsRegistry mock
│ └── TransparentUpgradeCheckerMock.sol # TransparentUpgradeChecker mock
│
├── test/
│ ├── BeaconUpgradeChecker.js # Beacon tests
│ ├── TransparentUpgradeChecker.js # Transparent tests
│ ├── UUPSUpgradeChecker.js # UUPS tests
│ ├── InterfaceIdsRegistry.js # InterfaceIdsRegistry tests
│ └── UpgradeCheckerFixture.js # Test fixture
│
├── artifacts/ # Compiled contract artifacts
├── coverage/ # Code coverage reports
├── ignition/ # Hardhat Ignition deployment modules
├── hardhat.config.js # Hardhat configuration
├── package.json # Dependencies and package info
├── slither.config.json # Slither analyzer config
└── README.md # This file
Abstract contract providing validation logic for proxy contracts.
Key functions:
_checkContractName(address impl)- Verify implementation name via staticcall_checkInterfaces(address impl, bytes4[] memory ids)- Validate ERC165 interface ids supporting_checkOverall(address impl, bytes4[] memory ids)- Execute both checks_targetContractName()- Returns expected implementation name (must override)
Tip
If ERC165 interface validation is not required, use the _checkContractName() or _checkContractNameBeforeFallback() functions only. In this case, there is no need for the InterfaceIdsRegistry library.
Abstract contract for upgrade-safe implementations.
Requirements:
- Must override
contractName()to return a unique identifier - Must override
supportsInterface()to declare supported interfaces
Warning
If interface checking is enabled and the _checkOverall() or _checkInterfaces() functions are used for validating, supportsInterface() MUST support all interfaces listed in InterfaceIdsRegistry.getInterfaceIds() of the proxy contract, along with IUpgradeChecker and IERC165 interfaces by default.
Manages persistent configuration using ERC7201 namespaced storage.
Key functions:
setInterfaceId(bytes4 id, bool add)- Register/unregister required interface (access restriction required)getInterfaceIds()- Returns all registered interfacesenableInterfacesCheck(bool enable)- Toggle validation on/off (access restriction required)getInterfacesCheckEnabled()- Returns current validation state
ERC7201 Storage location: 0xb3567140b780d0e6eae18a93d996909c6c854e99daead678dce9f5547099f300
Tip
The InterfaceIdsRegistry stores interfaceIds that MUST be supported by the implementation. The implementation returns the interfaceIds it supports via ERC165.supportsInterface.
Warning
The storage used for InterfaceIdsRegistry MUST always be located in the proxy contract. Calls to the library's setter functions MUST be made in the context of the proxy contract. The logic can be located in the implementation, depending on the proxy pattern.
constructorperforms initial validation- The validation function must be added to the
upgradeTo()function
constructorperforms initial validation- Detects upgrade calls via
msg.sender == ERC1967Utils.getAdmin()andmsg.sig == ITransparentUpgradeableProxy.upgradeToAndCall.selector - Provides
_checkOverallBeforeFallback(),_checkContractNameBeforeFallback(), and_checkInterfacesBeforeFallback()validation functions - The
_check*BeforeFallback()validation function must be added to the_fallback()function
Warning
The default TransparentUpgradeChecker implementation is configured for use with the OpenZeppelin implementation of TransparentUpgradeableProxy using ERC1967 and ITransparentUpgradeableProxy interface. For other implementations, you MUST override the _detectUpgradeCall() function and add custom logic for detecting upgrade calls and a desired validation function call.
- Combines the checker with the implementation logic (following the
UUPSpattern) - The validation function must be added to the
_authorizeUpgrade()function
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {UpgradeCheckerImplementation} from "@revxchain/upgrade-checker/UpgradeCheckerImplementation.sol";
import {InterfaceIdsRegistry} from "@revxchain/upgrade-checker/libraries/InterfaceIdsRegistry.sol";
import {BeaconUpgradeChecker} from "@revxchain/upgrade-checker/beacon/BeaconUpgradeChecker.sol";
// Your beacon contract that manages upgrade, must inherit from {BeaconUpgradeChecker}
contract MyBeacon is UpgradeableBeacon, BeaconUpgradeChecker {
constructor(
address implementation,
address initialOwner
) BeaconUpgradeChecker(implementation) UpgradeableBeacon(implementation, initialOwner) {
// Add optional {interfaceIds} setter and interfaces validation during deploy
InterfaceIdsRegistry.setInterfaceId(type(IMyUpgradeable).interfaceId, true);
_checkInterfaces(implementation, InterfaceIdsRegistry.getInterfaceIds());
}
// Override {upgradeTo} to add upgrade validation
function upgradeTo(address newImplementation) public override onlyOwner() {
// Validation runs here - upgrade fails if checks don't pass
_checkOverall(newImplementation, InterfaceIdsRegistry.getInterfaceIds());
super.upgradeTo(newImplementation);
}
// Override {_targetContractName} to assign required implementation's {contractName}
function _targetContractName() internal view virtual override returns(string memory targetContractName) {
return "MyBeaconImplementation";
}
// Add {InterfaceIdsRegistry} functions to manage required interface Ids
function enableInterfacesCheck(bool enable) external onlyOwner() {
InterfaceIdsRegistry.enableInterfacesCheck(enable);
}
function setInterfaceId(bytes4 interfaceId, bool add) external onlyOwner() {
InterfaceIdsRegistry.setInterfaceId(interfaceId, add);
}
function getInterfacesCheckEnabled() external view returns(bool isInterfacesCheckEnabled) {
return InterfaceIdsRegistry.getInterfacesCheckEnabled();
}
function getInterfaceIds() external view returns(bytes4[] memory interfaceIds) {
return InterfaceIdsRegistry.getInterfaceIds();
}
}
// Your implementation must inherit from {UpgradeCheckerImplementation}
contract MyBeaconImplementation is UpgradeCheckerImplementation {
// Override {contractName} to match beacon's {_targetContractName}
function contractName() public pure override returns(string memory) {
return "MyBeaconImplementation";
}
// Override {supportsInterface} to follow default {IERC165} rules
function supportsInterface(bytes4 interfaceId) public view virtual override returns(bool) {
return interfaceId == type(IMyBeaconImplementation).interfaceId || super.supportsInterface(interfaceId);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
import {TransparentUpgradeChecker} from "@revxchain/upgrade-checker/transparent/TransparentUpgradeChecker.sol";
import {UpgradeCheckerImplementation} from "@revxchain/upgrade-checker/UpgradeCheckerImplementation.sol";
import {InterfaceIdsRegistry} from "@revxchain/upgrade-checker/libraries/InterfaceIdsRegistry.sol";
// Your transparent proxy contract must inherit from {TransparentUpgradeChecker}
contract MyTransparentProxy is TransparentUpgradeChecker, TransparentUpgradeableProxy {
constructor(
address newImplementation,
address initialOwner,
bytes memory data
) TransparentUpgradeChecker(newImplementation) TransparentUpgradeableProxy(newImplementation, initialOwner, data) {
// Add optional {interfaceIds} setter and interfaces validation during deploy
InterfaceIdsRegistry.setInterfaceId(type(IAccessControl).interfaceId, true);
_checkInterfaces(newImplementation, InterfaceIdsRegistry.getInterfaceIds());
}
// Override {_targetContractName} to assign required implementation's {contractName}
function _targetContractName() internal pure override returns(string memory targetContractName) {
return "MyTransparentImplementation";
}
// Override {_fallback} to add upgrade validation
function _fallback() internal override {
_checkOverallBeforeFallback();
super._fallback();
}
}
// Your implementation must inherit from {UpgradeCheckerImplementation}
contract MyTransparentImplementation is UpgradeCheckerImplementation, AccessControlUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address defaultAdmin) external initializer {
__AccessControl_init();
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
}
// Override {contractName} to match proxy's {_targetContractName}
function contractName() public pure override returns(string memory) {
return "MyTransparentImplementation";
}
// Override {supportsInterface} to follow default {IERC165} rules
function supportsInterface(
bytes4 interfaceId
) public view virtual override(AccessControlUpgradeable, UpgradeCheckerImplementation) returns(bool) {
return
UpgradeCheckerImplementation.supportsInterface(interfaceId) ||
AccessControlUpgradeable.supportsInterface(interfaceId);
}
// Add {InterfaceIdsRegistry} functions to manage required interface Ids
function enableInterfacesCheck(bool enable) external onlyRole(DEFAULT_ADMIN_ROLE) {
InterfaceIdsRegistry.enableInterfacesCheck(enable);
}
function setInterfaceId(bytes4 interfaceId, bool add) external onlyRole(DEFAULT_ADMIN_ROLE) {
InterfaceIdsRegistry.setInterfaceId(interfaceId, add);
}
function getInterfacesCheckEnabled() external view returns(bool isInterfacesCheckEnabled) {
return InterfaceIdsRegistry.getInterfacesCheckEnabled();
}
function getInterfaceIds() external view returns(bytes4[] memory interfaceIds) {
return InterfaceIdsRegistry.getInterfaceIds();
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
import {UpgradeCheckerImplementation} from "@revxchain/upgrade-checker/UpgradeCheckerImplementation.sol";
import {InterfaceIdsRegistry} from "@revxchain/upgrade-checker/libraries/InterfaceIdsRegistry.sol";
import {UUPSUpgradeChecker} from "@revxchain/upgrade-checker/uups/UUPSUpgradeChecker.sol";
// Your UUPS implementation must inherit from {UUPSUpgradeChecker}
contract MyUUPSImplementation is AccessControlUpgradeable, UUPSUpgradeChecker, UUPSUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address defaultAdmin) external initializer {
__AccessControl_init();
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
}
// Override {contractName} to return a unique identifier
function contractName() public pure override returns(string memory) {
return "MyUUPSImplementation";
}
// Override {supportsInterface} to follow default {IERC165} rules
function supportsInterface(
bytes4 interfaceId
) public view virtual override(AccessControlUpgradeableUpgradeCheckerImplementation) returns(bool) {
return
AccessControlUpgradeable.supportsInterface(interfaceId) ||
UpgradeCheckerImplementation.supportsInterface(interfaceId);
}
// Add {InterfaceIdsRegistry} functions to manage interfaces
function enableInterfacesCheck(bool enable) external onlyRole(DEFAULT_ADMIN_ROLE) {
InterfaceIdsRegistry.enableInterfacesCheck(enable);
}
function setInterfaceId(bytes4 interfaceId, bool add) external onlyRole(DEFAULT_ADMIN_ROLE) {
InterfaceIdsRegistry.setInterfaceId(interfaceId, add);
}
function getInterfacesCheckEnabled() external view returns(bool isInterfacesCheckEnabled) {
return InterfaceIdsRegistry.getInterfacesCheckEnabled();
}
function getInterfaceIds() external view returns(bytes4[] memory interfaceIds) {
return InterfaceIdsRegistry.getInterfaceIds();
}
// Override {_authorizeUpgrade} to add upgrade validation and authorization
function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {
_checkOverall(newImplementation, InterfaceIdsRegistry.getInterfaceIds());
// You can hardcode the required interfaces if {InterfaceIdsRegistry} management is unnecessary.
// This may be changed in the next upgrade. Not recommended for use with other proxy patterns.
//
// bytes4[] memory _interfaceIds = new bytes4[](1);
// _interfaceIds[0] = type(IAccessControl).interfaceId;
// _checkOverall(newImplementation, _interfaceIds);
}
}MIT License
Permission is hereby granted to use, copy, modify, and distribute this software freely.
See LICENSE file for full terms.
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Submit a pull request
- GitHub Issues - Report bugs