Skip to content

Latest commit

 

History

History
333 lines (250 loc) · 9.55 KB

upgrade.md

File metadata and controls

333 lines (250 loc) · 9.55 KB
title description keywords displayed_sidebar tags
Deploying Upgradeable Contracts using Hardhat
Learn how to deploy upgradeable contracts on Conflux eSpace using Hardhat
tutorial
smart contracts
upgradeable contracts
Hardhat
eSpace
proxy pattern
eSpaceSidebar
Tutorial
Upgradeable Contracts
Hardhat

Deploying Upgradeable Contracts on Conflux eSpace using Hardhat

Before diving into the tutorial, let's briefly explain the basic principles of implementing upgradeable contracts:

  1. Separation of Concerns: Contract logic is separated from storage using two contracts:

    • A Proxy contract that holds the state and receives user interactions.
    • A Logic contract (Implementation contract) that contains the actual code logic.
  2. Delegated Calls: The Proxy contract uses delegatecall to forward function calls to the Logic contract.

  3. Upgradability: Upgrade by deploying a new Logic contract and updating the Proxy to point to it.

  4. Fallback Function: The Proxy contract uses a fallback function to catch and delegate all function calls.

  5. Storage Layout: Ensure new versions of the Logic contract maintain the same storage layout to prevent data corruption.

The workflow of upgradeable contracts is as follows:

  1. The Proxy contract stores the address of the current Logic contract.
  2. When the Proxy is called, it triggers the fallback function.
  3. The fallback function uses delegatecall to forward the call to the Logic contract.
  4. The Logic contract executes the function in the context of the Proxy's storage.
  5. To upgrade, deploy a new Logic contract and update the Proxy's reference.

This pattern allows for upgrading contract logic while preserving the contract's state and address, providing a seamless experience for users and other contracts interacting with the upgradeable contract.

Next, we'll proceed with the tutorial on how to implement this pattern on Conflux eSpace using Hardhat.

1. Project Setup

First, ensure you have Node.js and npm installed. Then, create a new directory and initialize the project:

mkdir upgradeable-contract-demo
cd upgradeable-contract-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox dotenv

Next, initialize the Hardhat project and select the JavaScript default template:

npx hardhat

When prompted, choose "Create a JavaScript project". This will create a basic Hardhat project structure, including contracts, scripts, and test directories, as well as a default hardhat.config.js file.

After completing these steps, you'll have a basic Hardhat project structure using JavaScript, ready for writing and deploying upgradeable contracts.

2. Configure Hardhat

Create a Hardhat configuration file:

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: "0.8.24",
  networks: {
    eSpaceTestnet: {
      url: "https://evmtestnet.confluxrpc.com",
      accounts: [process.env.PRIVATE_KEY],
    },
  },
};

Create a .env file and add your private key:

PRIVATE_KEY=your_private_key_here

3. Write Smart Contracts

Create a contracts directory and add the following contracts:

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

contract SimpleUpgrade {
    // Address of the current implementation contract
    address public implementation;
    // Address of the admin who can upgrade the contract
    address public admin;
    // A string variable to demonstrate state changes
    string public words;

    // Constructor sets the initial implementation and admin
    constructor(address _implementation) {
        admin = msg.sender;
        implementation = _implementation;
    }

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

    // Receive function to accept Ether
    receive() external payable {
    }

    // Function to upgrade the implementation contract
    // Only the admin can call this function
    function upgrade(address newImplementation) external {
        require(msg.sender == admin, "Only admin can upgrade");
        implementation = newImplementation;
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;


contract Logic1 {
    // Address of the current implementation contract
    address public implementation;
    // Address of the admin who can upgrade the contract
    address public admin;
    // A string variable to demonstrate state changes
    string public words;

    // Function to set the 'words' variable
    function foo() public {
        words = "old";
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Logic2 {
    // Address of the current implementation contract
    address public implementation;
    // Address of the admin who can upgrade the contract
    address public admin;
    // A string variable to demonstrate state changes
    string public words;

    // Function to set the 'words' variable
    // Note: This function is different from Logic1
    function foo() public {
        words = "new";
    }
}

4. Deployment Script

Create a scripts directory and add the following script:

const hre = require("hardhat");

async function main() {
  // Deploy Logic1 contract
  const Logic1 = await hre.ethers.getContractFactory("Logic1");
  const logic1 = await Logic1.deploy();
  await logic1.waitForDeployment();
  console.log("Logic1 deployed to:", await logic1.getAddress());

  // Deploy SimpleUpgrade (Proxy) contract
  const SimpleUpgrade = await hre.ethers.getContractFactory("SimpleUpgrade");
  const proxy = await SimpleUpgrade.deploy(await logic1.getAddress());
  await proxy.waitForDeployment();
  console.log("Proxy deployed to:", await proxy.getAddress());
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

5. Upgrade Script

const hre = require("hardhat");

async function main() {
  // Address of the deployed proxy contract
  const proxyAddress = "YOUR_PROXY_CONTRACT_ADDRESS";

  // Deploy Logic2 contract
  const Logic2 = await hre.ethers.getContractFactory("Logic2");
  const logic2 = await Logic2.deploy();
  await logic2.waitForDeployment();
  console.log("Logic2 deployed to:", await logic2.getAddress());

  // Attach to the existing proxy contract
  const SimpleUpgrade = await hre.ethers.getContractFactory("SimpleUpgrade");
  const proxy = SimpleUpgrade.attach(proxyAddress);

  // Log current contract information
  console.log("Admin address:", await proxy.admin());
  console.log("Current implementation:", await proxy.implementation());
  console.log("New implementation address:", await logic2.getAddress());

  // Get the signer (account that will send the transaction)
  const [signer] = await hre.ethers.getSigners();
  console.log("Caller address:", await signer.getAddress());

  // Upgrade the proxy to point to the new implementation
  await proxy.upgrade(await logic2.getAddress(), {
    gasLimit: 1000000,
    maxFeePerGas: ethers.parseUnits("20", "gwei"),
    maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"),
  });
  console.log("Proxy upgraded to Logic2");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

6. Testing Before and After Upgrade

Create testBeforeUpgrade.js and testAfterUpgrade.js:

const hre = require("hardhat");

async function main() {
  // Address of the deployed proxy contract
  const proxyAddress = "YOUR_PROXY_CONTRACT_ADDRESS";

  // Attach to the proxy contract using Logic1 ABI
  const Logic1 = await hre.ethers.getContractFactory("Logic1");
  const proxy = Logic1.attach(proxyAddress);

  // Call the foo() function
  const tx = await proxy.foo();
  console.log("Waiting for on-chain confirmation...");
  await tx.wait();

  // Read the 'words' variable
  const words = await proxy.words();
  console.log("Words after calling Logic1's foo():", words);

  // Get the current implementation address
  const implementationAddress = await proxy.implementation();
  console.log("Current implementation address:", implementationAddress);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

The content of testAfterUpgrade.js is similar to testBeforeUpgrade.js, but you need to change Logic1 to Logic2 and expect the value of the words variable to be "new".

7. Deployment and Upgrade Process

  1. Compile the contracts:
npx hardhat compile
  1. Deploy the initial contract:
npx hardhat run scripts/deploy.js --network eSpaceTestnet
  1. Run the pre-upgrade test:
npx hardhat run scripts/testBeforeUpgrade.js --network eSpaceTestnet

Expected output:

Words after calling Logic1's foo(): old
Current implementation address: 0x...(Logic1's address)
  1. Upgrade the contract:
npx hardhat run scripts/upgrade.js --network eSpaceTestnet
  1. Run the post-upgrade test:
npx hardhat run scripts/testAfterUpgrade.js --network eSpaceTestnet

Expected output:

Words after calling Logic2's foo(): new
Current implementation address: 0x...(Logic2's address)

By following this process, you can successfully deploy, test, and upgrade smart contracts on Conflux eSpace. This example demonstrates how to use a proxy contract to achieve upgradeability, allowing you to update contract logic without changing the contract address.