DEV Community

Cover image for Upgradable Proxy Contracts 101 (Advanced)
Venkatesh R
Venkatesh R

Posted on

Upgradable Proxy Contracts 101 (Advanced)

Upgradable Proxy Contract Advanced Concepts:

Do you want to learn how proxy upgradable contracts work? Or Do you want to create your own proxy contract? Or Do you want to become a Senior Blockchain Developer by learning complex concepts?

Well, good news! you're in the right place.

If you are already familiar with proxy upgradable contracts and want to know more about its internal concepts ? Directly jump to the Getting Started section.

What is Upgradable Proxy Contracts ?

In the software development industry, software quality and stability heavily depend on software enhancements or bug fixes on the time to time basis. Since smart contracts are immutable in nature, we need advanced techniques to make our smart contracts upgradable while maintaining considerable immutability.

Proxy Upgradable Contract is a advanced technique which is being used for updating the existing smart contract in the event of bug fixes, new features, update the existing business logic etc..

Getting Started:

To upgrade the smart contract, we need to have a sample smart contract to get started with it. We got you covered, use the Counter contract.

contract CounterV1 {
    uint public count;

    function inc() external {
        count += 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Upgradable proxy contract technique/approach is achieved with the help of
1. Proxy Contract
2. Storage Layout Library Contract
3. Proxy Admin Contract

Image description

Image description

Image description

1. Proxy Contract:

Proxy contract is contract which is responsible for communicating with the implementation contract, maintaining storage layout and contract's state.

Proxy contract will communicate with the implementation contract via Delegate Call using the Yul Assembly Language (a low level inline language which will directly interact with EVM) which will execute the contract logic and updates the storage layout of the calling contract.

Delegate Call: delegatecall is a low level function call similar to call. When contract A executes delegatecall to contract B, B's code is executed with contract A's storage

contract Proxy {
    address public admin;
    address public implementation;

    constructor() {
        admin = msg.sender;
    }

    modifier ifAdmin() {
        if (msg.sender == admin) {
            _;
        } else {
            _fallback();
        }
    }

    function upgradeTo(address _implementation) external ifAdmin {
        implementation = _implementation;
    }

    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(t, f, s) - copy s bytes from calldata at position f to mem at position t
            // calldatasize() - size of call data in bytes
            calldatacopy(0, 0, calldatasize())

            // Call the implementation.
            // out and outsize are 0 because we don't know the size yet.

            // delegatecall(g, a, in, insize, out, outsize) -
            // - call contract at address a
            // - with input mem[in…(in+insize))
            // - providing g gas
            // - and output area mem[out…(out+outsize))
            // - returning 0 on error (eg. out of gas) and 1 on success
            let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)

            // Copy the returned data.
            // returndatacopy(t, f, s) - copy s bytes from returndata at position f to mem at position t
            // returndatasize() - size of the last returndata
            returndatacopy(0, 0, returndatasize())

            switch result
            // delegatecall returns 0 on error.
            case 0 {
                // revert(p, s) - end execution, revert state changes, return data mem[p…(p+s))
                revert(0, returndatasize())
            }
            default {
                // return(p, s) - end execution, return data mem[p…(p+s))
                return(0, returndatasize())
            }
        }
    }

    function _fallback() private {
        _delegate(_getImplementation());
    }

    fallback() external payable {
        _fallback();
    }

    receive() external payable {
        _fallback();
    }
}
Enter fullscreen mode Exit fullscreen mode

We will always start interacting with the proxy contracts which will perform a delegatecall to implementation contract to execute the contract's logic.

function _delegate() is resposible for performing a delegate call. We will be initiating the contract call with the help of the Function Signature (eg. function inc()) which will be sent as msg.data to the contract via transaction. The receive() / fallback() function will be triggered when the transaction doesn't matches any other function signatures in the proxy contract, which will executes the function _delegate() function.

receive() - The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).

fallback() - If no receive function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception.

calldatacopy() - It will copy the calldata to the memory address 0 till the size of the calldata

calldatasize() - It is used to determine the size of the calldata in the executing transaction

returndatacopy() - It will copy the return data to the memory address 0 till the size of the result of the function call

returndatasize() - It is used to determine the size of the return data in the executing transaction

2. Storage Layout Library Contract:

Storage layout library contract is used to store and maintain the contract state variables without any collision/conflicts between each other. This is achieved by storing the contract's state variables in the different EVM sotrage slots with the help of Yul assembly language.

library StorageSlot {
    struct AddressSlot {
        address value;
    }

    function getAddressSlot(bytes32 slot)
        internal
        pure
        returns (AddressSlot storage r)
    {
        assembly {
            r.slot := slot
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

EVM consist of a storage slots which can store upto 256 bits of data in it. We are generating a hash based on the function signature which determines the location of the data to be stored in the storage slot.

contract TestProxy {
    bytes32 public constant slot = keccak256("SAMPLE_SLOT");

    function getSlot() external view returns (address) {
        return StorageSlot.getAddressSlot(slot).value;
    }

    function writeSlot(address _addr) external {
        StorageSlot.getAddressSlot(slot).value = _addr;
    }
}
Enter fullscreen mode Exit fullscreen mode

slot - Smart contract values are stored in slots, starting from slot 0 and so on. Elementary fixed-size value types occupy one slot. Moreover, they sometimes can be packed into one slot and unpacked on the fly

3. Proxy Admin Contract:

Proxy Admin Contract is used for managing the proxy contract. Using the Proxy Admin Contract we can look up, change, or upgrade the contract and its ownership such as

  1. Upgrade Implementation Contract
  2. Change Proxy Admin
  3. Lookup proxy admin and implementation contract
contract ProxyAdmin {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function getProxyAdmin(address proxy) external view returns (address) {
        (bool ok, bytes memory res) = proxy.staticcall(
            abi.encodeCall(Proxy.implementation, ())
        );
        require(ok, "call failed");
        return abi.decode(res, (address));
    }

    function getProxyImplementation(address proxy) external view returns (address) {
        (bool ok, bytes memory res) = proxy.staticcall(abi.encodeCall(Proxy.admin, ()));
        require(ok, "call failed");
        return abi.decode(res, (address));
    }

    function changeProxyAdmin(address payable proxy, address admin) external onlyOwner {
        Proxy(proxy).changeAdmin(admin);
    }

    function upgrade(address payable proxy, address implementation) external onlyOwner {
        Proxy(proxy).upgradeTo(implementation);
    }
}
Enter fullscreen mode Exit fullscreen mode

Upgrade The Contract:

To upgrade a smart contract, we need a V2 implementation Counter contract.

contract CounterV2 {
    uint public count;

    function inc() external {
        count += 1;
    }

    function dec() external {
        count -= 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

We can use the above contract to upgrade the existing V1 Counter contract using proxy upgradable technique.

Deploy the V2 contract in the blockchain and use the deployed V2 contract address to upgrade the old version.

To upgrade the V1 Counter contract, we need to trigger the function upgrade() in the Proxy Admin Contract by pointing the proxy contract's address. The Proxy Admin Contract will replace the V1 implementation contract address with the V2 contract address without affecting the contract's state. While upgrading the implementation contract, the address of the proxy contract will never change. Hence the proxy contract will now get a new implementation contract with new features/bug fixes.

Hope you guys have learnt something new today, do follow me to get more exiting contents on #blockchain #Web3 #SmartContracts #Solidity #Rust #Solana

You can reach out to me via LinkedIn, Github

Top comments (0)