Understanding the Proxy Pattern

The Proxy pattern involves two main components: the Proxy contract and the Logic contract. The Proxy contract acts as a mediator between users and the Logic contract, forwarding calls to the appropriate function in the Logic contract. This separation allows you to upgrade the Logic contract without losing the state stored in the Proxy contract.

Key Concepts

ConceptDescription
Proxy ContractA contract that delegates calls to the Logic contract while holding state.
Logic ContractThe contract containing the business logic that can be upgraded.
Storage LayoutThe arrangement of state variables in the contract's storage.

Setting Up the Environment

To get started, ensure you have the following tools installed:

  • Node.js
  • Truffle or Hardhat
  • Ganache (for local blockchain testing)

You can initialize a new project by running:

mkdir upgradeable-smart-contracts
cd upgradeable-smart-contracts
npm init -y
npm install --save-dev truffle

Step 1: Creating the Logic Contract

Create a new file named Logic.sol in the contracts directory. This contract will contain the logic that we want to upgrade later.

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

contract Logic {
    uint public value;

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

    function increment() public {
        value++;
    }
}

Step 2: Creating the Proxy Contract

Next, create a file named Proxy.sol in the contracts directory. This contract will handle the delegation of calls to the Logic contract.

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

contract Proxy {
    address public logicContract;

    constructor(address _logicContract) {
        logicContract = _logicContract;
    }

    fallback() external payable {
        address _impl = logicContract;
        require(_impl != address(0), "Logic contract 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()) }
        }
    }
}

Step 3: Deploying the Contracts

Create a migration file in the migrations directory named 2_deploy_contracts.js to deploy both contracts.

const Logic = artifacts.require("Logic");
const Proxy = artifacts.require("Proxy");

module.exports = async function (deployer) {
    await deployer.deploy(Logic);
    const logicInstance = await Logic.deployed();
    await deployer.deploy(Proxy, logicInstance.address);
};

Step 4: Interacting with the Contracts

Once the contracts are deployed, you can interact with them using Truffle Console. Start the console with:

truffle console

Then, run the following commands to interact with your contracts:

let proxy = await Proxy.deployed();
let logicAddress = await proxy.logicContract();

let logic = await Logic.at(logicAddress);
await logic.setValue(42);
let value = await logic.value(); // Should return 42

Step 5: Upgrading the Logic Contract

To upgrade the Logic contract, create a new version of the Logic contract. For instance, create LogicV2.sol:

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

contract LogicV2 {
    uint public value;

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

    function increment() public {
        value++;
    }

    function decrement() public {
        value--;
    }
}

Deploy the new Logic contract and update the Proxy contract to point to the new Logic contract:

const LogicV2 = artifacts.require("LogicV2");

module.exports = async function (deployer) {
    await deployer.deploy(LogicV2);
    const logicV2Instance = await LogicV2.deployed();
    const proxy = await Proxy.deployed();
    proxy.logicContract = logicV2Instance.address; // Update the logic contract address
};

Best Practices for Upgradeable Contracts

  • Use a well-defined interface: Ensure that your Logic contracts adhere to a consistent interface to avoid breaking changes.
  • Implement access control: Use modifiers to restrict who can upgrade the contract.
  • Thoroughly test upgrades: Always test the new Logic contract before deploying it to ensure it works as expected.

Conclusion

Writing upgradeable smart contracts in Solidity using the Proxy pattern allows for flexibility and adaptability in your decentralized applications. By separating the logic from the state, you can ensure that your contracts remain functional and secure over time.

Learn more with useful resources