Deploying Upgradeable Contracts on Conflux eSpace using Hardhat
Before diving into the tutorial, let's briefly explain the basic principles of implementing upgradeable contracts:
-
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.
-
Delegated Calls: The Proxy contract uses
delegatecall
to forward function calls to the Logic contract. -
Upgradability: Upgrade by deploying a new Logic contract and updating the Proxy to point to it.
-
Fallback Function: The Proxy contract uses a fallback function to catch and delegate all function calls.
-
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:
- The Proxy contract stores the address of the current Logic contract.
- When the Proxy is called, it triggers the fallback function.
- The fallback function uses
delegatecall
to forward the call to the Logic contract. - The Logic contract executes the function in the context of the Proxy's storage.
- 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);
});