Simple implementation

pragma solidity ^0.8.0;

contract Proxy {
    address public implementation;

    constructor(address _implementation) {
        implementation = _implementation;
    }

    fallback() external payable {
        // Forward the call to the implementation contract
        (bool success, ) = implementation.delegatecall(msg.data);
        require(success, "Delegatecall failed");
    }
}

contract ImplementationV1 {
    uint public num;
    address public implementation;

    function setNum(uint _num) public {
        num = _num;
    }
}

Try calling the proxy contract with the following callData

0xcd16ecbf0000000000000000000000000000000000000000000000000000000000000002

<aside> 💡

This calldata comes from the Keccak-256 hash of setNum(uint256) followed by 1 (32 bytes)

</aside>

Complex implementation

Ref - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol

Proxy.sol

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/Proxy.sol)

pragma solidity ^0.8.20;

abstract contract Proxy {
    function _delegate(address implementation) internal virtual {
        assembly {
            // Copy msg.data. We take full control of memory in this inline assembly
            // block because it will not return to Solidity code. We overwrite the
            // Solidity scratch pad at memory position 0.
            calldatacopy(0, 0, calldatasize())

            // Call the implementation.
            // out and outsize are 0 because we don't know the size yet.
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

            // Copy the returned data.
            returndatacopy(0, 0, returndatasize())

            switch result
            // delegatecall returns 0 on error.
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }

    /**
     * @dev This is a virtual function that should be overridden so it returns the address to which the fallback
     * function and {_fallback} should delegate.
     */
    function _implementation() internal view virtual returns (address);

    /**
     * @dev Delegates the current call to the address returned by `_implementation()`.
     *
     * This function does not return to its internal call site, it will return directly to the external caller.
     */
    function _fallback() internal virtual {
        _delegate(_implementation());
    }

    /**
     * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other
     * function in the contract matches the call data.
     */
    fallback() external payable virtual {
        _fallback();
    }
}

Using Proxy.sol

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/Proxy.sol";

contract StorageProxy is Proxy {
    uint256 public num;
    address public implementation;

    constructor(address _implementation) {
        implementation = _implementation;
    }

    function _implementation() internal view virtual override returns (address) {
        return implementation;
    }
}

contract ImplementationV1 {
    uint256 public num;
    address public implementation;

    function setNum(uint256 _num) public {
        num = _num;
    }
}

Problems with this approach -

How do we upgrade implementation (you might say I will add a new function called upgradeImplementation but what if there is a similar function in the implementation contract?