@revxchain/upgrade-checker
v1.0.0
Published
Onchain checker library for Solidity proxy contract upgrades
Downloads
14
Maintainers
Readme
Upgrade Checker
A Solidity library for validating proxy contract upgrades with runtime integrity checks supporting various proxy patterns.
Table of Contents
- Overview
- Main Features
- Installation
- Project Structure
- Key Components
- Quick Start
- License
- Contributing
- Support
Overview
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.
Main Features
1. Various proxy pattern support
- Beacon Proxy - Beacon-controlled upgrades
- Transparent Upgradeable Proxy - Proxy-controlled upgrades
- UUPS Proxy - Implementation-controlled upgrades
2. Dual validation strategy
- Contract Name Matching - Verify implementation identity via unique
contractName() - ERC165 Interface Checking - Required interfaceIds validation (optional)
3. Persistent registry system
- ERC7201 Namespaced Storage - Avoid storage collisions
- Dynamic Configuration - Add/remove interfaceIds requirements at runtime
- Interface Check Toggle - Enable/disable validation as needed
4. Developer-friendly
- Copy-paste ready examples for each upgradeable proxy pattern (Diamond TBA)
- Comprehensive test suite with 50+ test cases
- Clear error messages and documentation
Installation
Repository
Prerequisites
- Node.js 18.0.0 or later
- npm or yarn package manager
- Git for version control
Step 1: Clone the repository
git clone https://github.com/RevxChain/upgrade-checker.git
cd upgrade-checkerStep 2: Install dependencies
npm installThis installs:
- Hardhat - Ethereum development framework
- OpenZeppelin Contracts - Audited smart contract library
- Hardhat Plugins - Testing, upgrades, gas reporting, sizing
Step 3: Create environment file (optional)
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
Step 4: Compile contracts
npx hardhat compileCompiles all Solidity contracts and generates artifacts.
Step 5: Run tests
npx hardhat testStep 6: Run coverage
npx hardhat coverageStep 7: Run tests forking desired mainnet
Edit .env with your configuration:
# MAINNET FORK TEST
FORK_RPC_URL=https://eth.llamarpc.com
FORK_BLOCK_NUMBER=24540000Launch a local fork of the network in the first CLI:
npx hardhat nodeRun tests in the second CLI:
npx hardhat test --network localhostPackage
Hardhat (npm)
npm install @revxchain/upgrade-checkerThe package requires OpenZeppelin libraries as peer dependencies.
Install, if it is missing:
npm install @openzeppelin/[email protected]npm install @openzeppelin/[email protected]Project Structure
upgrade-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 fileKey Components
1. UpgradeChecker (Proxy-side base contract)
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
ERC165interface validation is not required, use the_checkContractName()or_checkContractNameBeforeFallback()functions only. In this case, there is no need for theInterfaceIdsRegistrylibrary.
2. UpgradeCheckerImplementation (Implementation-side base contract)
Abstract contract for upgrade-safe implementations.
Requirements:
- Must override
contractName()to return a unique identifier - Must override
supportsInterface()to declare supported interfaces
IMPORTANT: If interface checking is enabled and the
_checkOverall()or_checkInterfaces()functions are used for validating,supportsInterface()MUST support all interfaces listed inInterfaceIdsRegistry.getInterfaceIds()of the proxy contract, along withIUpgradeCheckerandIERC165interfaces by default.
3. InterfaceIdsRegistry (Storage library)
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
Storage location: 0xb3567140b780d0e6eae18a93d996909c6c854e99daead678dce9f5547099f300
IMPORTANT: The storage used for
InterfaceIdsRegistryMUST 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.
4. Pattern-specific implementations
BeaconUpgradeChecker
constructorperforms initial validation- The validation function must be added to the
upgradeTo()function
TransparentUpgradeChecker
constructorperforms initial validation- Detects upgrade calls via
msg.sender == ERC1967Utils.getAdmin()andmsg.sig == ITransparentUpgradeableProxy.upgradeToAndCall.selector - Provides
_checkOverallBeforeFallback(),_checkContractNameBeforeFallback(),_checkInterfacesBeforeFallback() - The
*BeforeFallback()validation function must be added to the_fallback()function
IMPORTANT: The default
TransparentUpgradeCheckerimplementation is configured for use with theOpenZeppelinimplementation ofTransparentUpgradeableProxyusing 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.
UUPSUpgradeChecker
- Combines the checker with the implementation logic (following the
UUPSpattern) - The validation function must be added to the
_authorizeUpgrade()function
Quick Start
Example: UpgradeableBeacon with Upgrade Checker
// 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(IMyUpgradeable).interfaceId || super.supportsInterface(interfaceId);
}
}
Example: TransparentUpgradeableProxy with Upgrade Checker
// 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();
}
}Example: UUPS with Upgrade Checker
// 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);
}
}License
MIT License
Permission is hereby granted to use, copy, modify, and distribute this software freely.
See LICENSE file for full terms.
Contributing
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Submit a pull request
Support
- GitHub Issues - Report bugs
