Understanding the Proxy Pattern

The Proxy pattern involves creating a contract (the proxy) that delegates calls to another contract (the logic contract). This separation allows for the logic contract to be upgraded without changing the address of the proxy contract, which holds the state.

Key Components

  1. Proxy Contract: Holds the state and delegates calls to the logic contract.
  2. Logic Contract: Contains the business logic that can be upgraded.
  3. Admin: An account or contract responsible for upgrading the logic contract.

Basic Implementation of a Proxy Contract

Here’s a simple implementation of a proxy contract using Solidity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Proxy {
    address public implementation;
    address public admin;

    constructor(address _implementation) {
        implementation = _implementation;
        admin = msg.sender;
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "Not authorized");
        _;
    }

    function upgrade(address _newImplementation) external onlyAdmin {
        implementation = _newImplementation;
    }

    fallback() external {
        address _impl = implementation;
        require(_impl != address(0), "Implementation not set");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Logic Contract Example

The logic contract can be defined as follows:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Logic {
    uint public value;

    function setValue(uint _value) external {
        value = _value;
    }

    function getValue() external view returns (uint) {
        return value;
    }
}

Deploying the Contracts

  1. Deploy the Logic Contract:
  • Deploy the Logic contract first to obtain its address.
  1. Deploy the Proxy Contract:
  • Pass the address of the Logic contract to the Proxy constructor.

Interaction with the Proxy

After deploying both contracts, you can interact with the proxy to call the logic contract's functions. For example, to set a value:

const proxyAddress = "0x..."; // Address of the deployed Proxy
const logicAddress = "0x..."; // Address of the deployed Logic

const proxyContract = new ethers.Contract(proxyAddress, proxyAbi, signer);
await proxyContract.setValue(42); // Calls Logic.setValue through the proxy

Upgrading the Logic Contract

To upgrade the logic, deploy a new version of the logic contract and call the upgrade function on the proxy:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract LogicV2 {
    uint public value;

    function setValue(uint _value) external {
        value = _value;
    }

    function incrementValue() external {
        value += 1;
    }

    function getValue() external view returns (uint) {
        return value;
    }
}

After deploying LogicV2, upgrade the proxy:

await proxyContract.upgrade(newLogicAddress); // Upgrade the proxy to LogicV2

Pros and Cons of the Proxy Pattern

ProsCons
Allows for contract upgradesIncreased complexity
Maintains state across upgradesPotential for delegatecall pitfalls
Enables bug fixes and feature additionsRequires careful management of access control

Best Practices for Upgradable Contracts

  1. Use Initializer Functions: Instead of constructors, use initializer functions for setting up state in logic contracts. This allows for proper initialization after deployment.
  1. Access Control: Implement strict access control to the upgrade function to prevent unauthorized upgrades.
  1. Testing: Thoroughly test both the proxy and logic contracts, especially after upgrades, to ensure that the state is preserved and the logic works as intended.
  1. Versioning: Maintain clear versioning of your logic contracts to avoid confusion during upgrades.
  1. Gas Optimization: Consider gas costs associated with delegate calls and storage layout to minimize expenses.

Conclusion

Implementing upgradable smart contracts using the Proxy pattern is a powerful technique in Solidity that facilitates the evolution of decentralized applications. By understanding the structure and best practices outlined in this guide, developers can create robust and maintainable contracts that can adapt to future needs.


Learn more with useful resources