# Intoducing VersaProxy smart contract

*An alternative to OpenZeppelin Upgradeable Contracts*

By [degengineering.ink](https://paragraph.com/@degengineering.ink) · 2025-03-05

#ethereum, #web3, #blockchain, #proxy, #upgrade

---

### What is VersaProxy in short?

VersaProxy is a proxy contract that supports multiple versions of a logic contract and routes calls to the correct version using a given version number.

![](https://storage.googleapis.com/papyrus_images/6af68dc2a1925030a67cca35c399c039.jpg)

#### Advantages

*   The proxy can route calls to the correct logic contract using a given `uint256` version number. This allows users and developers to manage migrations more smoothly.
    
*   Thanks to **ERC-7201 (Namespaced Storage Layout)**, developers can choose to have multiple versions of the logic contract working on the same storage slot or **reset** the state by changing the storage slot used by the new version of the logic contract.
    

#### Drawbacks

*   By convention, **all functions called through VersaProxy must have** `<uint256 version>` **as their first parameter**, even if the version number is not used by the logic contract.
    
*   **Libraries cannot be used as-is** and must be rewritten to include the version number as the first parameter of proxiable functions.
    

### Why VersaProxy?

Smart contracts are **immutable**—once deployed, they cannot be updated to fix bugs or add features. Until now, when you wanted to change the logic of your decentralized application, you basically had two options:

1.  **Deploy a new version of your smart contracts** and update all frontends to use the new version, or juggle between different contract versions.
    
    The two major drawbacks of this approach are:  
    a. **All frontends must be updated at once**, which can be a major issue in a truly decentralized environment.  
    b. **Migrating the state/storage of a smart contract is neither easy nor free**.
    
2.  **Use OpenZeppelin upgradeable contracts**, where a proxy contract delegates calls to a logic contract. When developers deploy a new version of the logic contract, they call a special function on the proxy contract to update the logic contract being used.
    
    A key benefit of this pattern is that the contract state and storage are **preserved**, as they belong to the proxy. However, the major drawback is the lack of **soft migration**:
    
    *   If the second version introduces breaking changes, users who haven't upgraded in time may find their clients broken.
        

With both approaches, **users are forced to migrate immediately** when the developer pushes an update. However, in some cases, this is not feasible, as not all users can update their clients simultaneously.

### VersaProxy: A Third Approach

VersaProxy was developed to provide an alternative way to manage smart contract upgrades **with a smooth transition between versions**. Developers have **more flexibility** to upgrade their smart contracts while allowing users to **migrate at their own pace** without breaking their clients.

Not everything will be possible, and not every option will be the right one.

**Be cautious.**

### How does VersaProxy works?

VersaProxy follows only two conventions:

1.  **Use ERC-7201 Namespaced Storage Layout** in your logic contract. This gives you fine-grained control over whether different logic contract versions share the same storage slot or not.
    
2.  **All functions of the logic contract that are called through the proxy must include a** `uint256` **parameter as the target version number**, even if the parameter is not used by the logic contract.
    

The **fallback function** of VersaProxy scans the calldata to automatically extract and use the target version number. It does this by:

*   Skipping the function selector.
    
*   Reading a 32-byte word that, by convention, represents the target version number.
    

The rest functions **similarly to OpenZeppelin's proxy contract**.

       /**
         * @dev Fallback function that proxies calls to the target implementation by using a version number
         * provided in the first 4 bytes of the calldata. The calldata will be forwarded to the implementation contract
         * withtout the first 4 bytes.
         */
        fallback() external payable {
            
            // Get the target version number as a uint256 from the first parameter in calldata.
            // 
            // The convention is all proxied functions in the implementation contracts MUST have the version number as the first parameter.
            // In order to achieve this, we need to skip the function selector (4 bytes) and read the first parameter as a uint256.
            uint256 version;
            assembly {
                // Extract the first parameter (version as uint256) from calldata
                version := calldataload(4)
            }
    
            // Get the implementation address for the provided version
            require(version < _nextVersion, "Unsupported version");
            require(_enabledVersions[version], "Version not enabled");
            address implementation = _implementations[version];
            //console.log("Target version is:", version);
            //console.log("Target implementation is:", implementation);
    
            // Regular delegatecall to the implementation contract as per Proxy pattern
            assembly {
                // Copy calldata to memory
                calldatacopy(0, 0, calldatasize())
    
                // Delegatecall to the implementation contract
                let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
    
                // Copy returndata to memory
                returndatacopy(0, 0, returndatasize())
    
                // Handle success or failure
                switch result
                case 0 { revert(0, returndatasize()) }
                default { return(0, returndatasize()) }
            }
        }

Below is an example of a smart contract function that has been rewritten to be used through VersaProxy (see source code: VersaOwnableV0.sol).

        /**
         * @param version Not used here. The version number use dby VersaProxy to route the call (convention)
         * @dev Returns the address of the current owner.
         */
        function owner(uint256 version) public view virtual returns (address) {
            VersaOwnableV0Storage storage $ = _getVersaOwnableV0Storage();
            return $._owner;
        }

### What are the next steps?

The next steps will be to create a public code repository providing the source code of VersaProxy, its tests, rewritten contract libraries, and examples.

### VersaProxy source code

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    
    import "./lib/openzeppelin/access/Ownable.sol";
    
    // Import Hardhat's console
    //import "hardhat/console.sol";
    
    /**
     * @title VersaProxy
     * @author @degengineering.ink
     * @notice This contract acts as a proxy dispatcher that routes calls to different versions of an implementation contract.
     * 
     * @dev VersaProxy allows multiple live versions of an implementation contract to co-exist. 
     * 
     * The goals of VersaProxy are to:
     * - Allow multiple releases of the implementation contract to coexist by the the time users migrate to the latest release. VersaProxy will preserve the storage and balance.
     * - Allow users to choose which version of the implementation contract will be called by the proxy by providing a uint256 version number as the first parameter of the calldata.
     * - Allow the owner to add new releases of an implementation contract.
     * - Allow the owner to update an existing version inplace.
     * - Allow the owner to enable/disable versions.
     */
    contract VersaProxy is Ownable {
    
        /*************************************************************************************************************************************/
        /* STORAGE                                                                                                                           */
        /*************************************************************************************************************************************/
    
        /**
         * @notice Array which contains the addresses of the implementation contracts for each version
         */ 
        address[] private _implementations;
    
        // Array of bool which indicates whether the version is enabled or not
        bool[] private _enabledVersions;
    
        // Next version number, starts at 0.
        uint256 private _nextVersion;
    
        /*************************************************************************************************************************************/
        /* CONSTRUCTOR                                                                                                                       */
        /*************************************************************************************************************************************/
    
        constructor() Ownable(msg.sender) {
            // Initialize the next version to 0
            _nextVersion = 0;
        }
    
        /*************************************************************************************************************************************/
        /* EVENTS                                                                                                                            */
        /*************************************************************************************************************************************/
    
        /**
         * @notice Emitted when a new version of the implementation contract is released.
         * @param version The version number of the release.
         * @param implementation The address of the implementation contract.
         */
        event Release(uint256 version, address implementation);
    
        /**
         * @notice Emitted when an existing release is updated inplace.
         * @param version The version number.
         * @param old The old address of the implementation contract.
         * @param current The new address of the implementation contract.
         */
        event Update(uint256 version, address old, address current);
    
        /**
         * @notice Emitted when a version is enabled.
         * @param version The number of the version that has been enabled.
         */
        event Enabled(uint256 version);
    
        /**
         * @notice Emitted when a version is disabled.
         * @param version The number of the version that has been disabled.
         */
        event Disabled(uint256 version);
    
        /*************************************************************************************************************************************/
        /* RELEASE MMANAGEMENT                                                                                                               */
        /*************************************************************************************************************************************/
    
        /**
         * Add a new release to this proxy and enable it.
         * @param implementation The address of the implementation contract.
         * @dev This function will assign the next version number to the provided release unless no release has been done yet (0). 
         * Only the owner can call this function.
         */
        function release(address implementation) public onlyOwner {
            // Create the new release
            uint256 current = _release(implementation);
            // Enable the new release
            _enable(current);
        }
    
        /**
         * Enable a specific version of the implementation contract.
         * @param version The version number to enable.
         * @dev Only the owner can call this function.
         */
        function enable(uint256 version) public onlyOwner {
            require(version < _nextVersion, "Unsupported version");
            require(!_enabledVersions[version], "Version already enabled");
            _enable(version);
        }
    
        /**
         * Disable a specific version of the implementation contract.
         * @param version The version number to disable.
         * @dev Only the owner can call this function.
         */
        function disable(uint256 version) public onlyOwner {
            require(version < _nextVersion, "Unsupported version");
            require(_enabledVersions[version], "Version already disabled");
            _disable(version);
        }
    
        /**
         * Update an existing release inplace and enable it.
         * @param version The version number of the release to update.
         * @param implementation the address of the new implementation contract.
         */
        function update(uint256 version, address implementation) public onlyOwner {
            require(version < _nextVersion, "Unsupported version");
            _update(version, implementation);
            _enable(version);
        }
    
        /**
         * Get the latest version number.
         */
        function latestVersion() public view returns (uint256) {
            return _nextVersion == 0 ? 0 : _nextVersion - 1;
        }
    
        /**
         * @dev Gets the implementation address for a specific version.
         * @param version The version number.
         * @return The address of the implementation contract.
         */
        function getImplementation(uint256 version) public view returns (address) {
            return _implementations[version];
        }
    
        /**
         * Add the provided address as new release to this proxy.
         * @param implementation The address of the implementation contract.
         * @dev This function will assign the next version number to the provided release unless no release has been done yet (0).
         */
        function _release(address implementation) internal returns (uint256) {
            // Add the implementation address to the implementations array
            _implementations.push(implementation);
            _enabledVersions.push(false);
            // Emit a Release event
            emit Release(_nextVersion, implementation);
            // Increment the next version number
            _nextVersion++;
            // Return the version number assigned to the release
            return latestVersion();
        }
    
        /**
         * Enable the provided version.
         * @param version The version number to enable.
         */
        function _enable(uint256 version) internal {
            // Set the version to enabled
            _enabledVersions[version] = true;
            // Emit an Enabled event
            emit Enabled(version);
        }
    
        /**
         * Disable the provided version.
         * @param version The version number to disable.
         */
        function _disable(uint256 version) internal {
            // Set the version to disabled
            _enabledVersions[version] = false;
            // Emit a Disabled event
            emit Disabled(version);
        }
    
        /**
         * Update the implementation address for a specific version.
         * @param version The version number.
         * @param implementation The address of the implementation contract.
         */
        function _update(uint256 version, address implementation) internal {
            // Get the old implementation address
            address old = _implementations[version];
            // Update the implementation address
            _implementations[version] = implementation;
            // Emit an Update event
            emit Update(version, old, implementation);
        }
    
        /**
         * @dev Fallback function that proxies calls to the target implementation by using a version number
         * provided in the first 4 bytes of the calldata. The calldata will be forwarded to the implementation contract
         * withtout the first 4 bytes.
         */
        fallback() external payable {
            
            // Get the target version number as a uint256 from the first parameter in calldata.
            // 
            // The convention is all proxied functions in the implementation contracts MUST have the version number as the first parameter.
            // In order to achieve this, we need to skip the function selector (4 bytes) and read the first parameter as a uint256.
            uint256 version;
            assembly {
                // Extract the first parameter (version as uint256) from calldata
                version := calldataload(4)
            }
    
            // Get the implementation address for the provided version
            require(version < _nextVersion, "Unsupported version");
            require(_enabledVersions[version], "Version not enabled");
            address implementation = _implementations[version];
            //console.log("Target version is:", version);
            //console.log("Target implementation is:", implementation);
    
            // Regular delegatecall to the implementation contract as per Proxy pattern
            assembly {
                // Copy calldata to memory
                calldatacopy(0, 0, calldatasize())
    
                // Delegatecall to the implementation contract
                let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
    
                // Copy returndata to memory
                returndatacopy(0, 0, returndatasize())
    
                // Handle success or failure
                switch result
                case 0 { revert(0, returndatasize()) }
                default { return(0, returndatasize()) }
            }
        }
    
        // Receive Ether when msg.data is empty
        receive() external payable {
            // Revert with a message: no donnations accepted
            revert("No donations accepted");
        }
    }
    

### VersaOwnable source code

    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.24;
    
    import {ContextUpgradeable} from "./lib/openzeppelin/utils/ContextUpgradeable.sol";
    import {Initializable} from "./lib/openzeppelin/proxy/utils/Initializable.sol";
    
    /**
     * @dev Contract module which provides a basic access control mechanism, where
     * there is an account (an owner) that can be granted exclusive access to
     * specific functions.
     *
     * The initial owner is set to the address provided by the deployer. This can
     * later be changed with {transferOwnership}.
     *
     * This module is used through inheritance. It will make available the modifier
     * `onlyOwner`, which can be applied to your functions to restrict their use to
     * the owner.
     * 
     * The only difference with OwnableUpgradeable is that all functions of the contract
     * which should be callable through the proxy include a uint256 as first parameter by
     * convention. This uint256 is a version number that is used by VersaProxy to route
     * calls to the right live version of the implementation contract.
     */
    abstract contract VersaOwnableV0 is Initializable, ContextUpgradeable {
    
        /// @custom:storage-location erc7201:openzeppelin.storage.VersaOwnableV0
        struct VersaOwnableV0Storage {
            address _owner;
        }
    
        // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.VersaOwnableV0")) - 1)) & ~bytes32(uint256(0xff))
        bytes32 private constant VersaOwnableV0StorageLocation = 0x0f67f515d92eae41c3e9af6441886123951ac11f3028c192bb6c729582a00700;
    
        function _getVersaOwnableV0Storage() private pure returns (VersaOwnableV0Storage storage $) {
            assembly {
                $.slot := VersaOwnableV0StorageLocation
            }
        }
    
        /**
         * @dev The caller account is not authorized to perform an operation.
         */
        error OwnableUnauthorizedAccount(address account);
    
        /**
         * @dev The owner is not a valid owner account. (eg. `address(0)`)
         */
        error OwnableInvalidOwner(address owner);
    
        event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
        /**
         * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
         */
        function __Ownable_init(address initialOwner) internal onlyInitializing {
            __Ownable_init_unchained(initialOwner);
        }
    
        function __Ownable_init_unchained(address initialOwner) internal onlyInitializing {
            if (initialOwner == address(0)) {
                revert OwnableInvalidOwner(address(0));
            }
            _transferOwnership(initialOwner);
        }
    
        /**
         * @dev Throws if called by any account other than the owner.
         */
        modifier onlyOwner() {
            _checkOwner();
            _;
        }
    
        /**
         * @param version Not used here. The version number use dby VersaProxy to route the call (convention)
         * @dev Returns the address of the current owner.
         */
        function owner(uint256 version) public view virtual returns (address) {
            VersaOwnableV0Storage storage $ = _getVersaOwnableV0Storage();
            return $._owner;
        }
    
        /**
         * @dev Throws if the sender is not the owner.
         */
        function _checkOwner() internal view virtual {
            if (owner(0) != _msgSender()) {
                revert OwnableUnauthorizedAccount(_msgSender());
            }
        }
    
        /**
         * @param version Not used here. The version number use dby VersaProxy to route the call (convention)
         * @dev Leaves the contract without owner. It will not be possible to call
         * `onlyOwner` functions. Can only be called by the current owner.
         *
         * NOTE: Renouncing ownership will leave the contract without an owner,
         * thereby disabling any functionality that is only available to the owner.
         */
        function renounceOwnership(uint256 version) public virtual onlyOwner {
            _transferOwnership(address(0));
        }
    
        /**
         * @param version Not used here. The version number use dby VersaProxy to route the call (convention)
         * @dev Transfers ownership of the contract to a new account (`newOwner`).
         * Can only be called by the current owner.
         */
        function transferOwnership(uint256 version, address newOwner) public virtual onlyOwner {
            if (newOwner == address(0)) {
                revert OwnableInvalidOwner(address(0));
            }
            _transferOwnership(newOwner);
        }
    
        /**
         * @dev Transfers ownership of the contract to a new account (`newOwner`).
         * Internal function without access restriction.
         */
        function _transferOwnership(address newOwner) internal virtual {
            address oldOwner = owner(0); // Remember: version number is not used internally
            VersaOwnableV0Storage storage $ = _getVersaOwnableV0Storage();
            $._owner = newOwner;
            emit OwnershipTransferred(oldOwner, newOwner);
        }
    }

---

*Originally published on [degengineering.ink](https://paragraph.com/@degengineering.ink/intoducing-versaproxy-smart-contract)*
