🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples Hardhat Development Framework
Exemples de l'environnement de développement Ethereum Hardhat incluant les contrats intelligents, les tests, le déploiement, les scripts, les plugins et les flux de travail de développement
💻 Hardhat Hello World - Configuration de Base javascript
Configuration complète de projet Hardhat avec contrats intelligents, tests, scripts de déploiement et configuration
// Hardhat Hello World - Complete Project Setup
// Initialize: npx hardhat init
// ===== hardhat.config.js =====
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
hardhat: {
chainId: 1337,
// Accounts configuration for local testing
accounts: {
count: 10,
accountsBalance: "10000000000000000000000", // 10000 ETH
},
},
localhost: {
url: "http://127.0.0.1:8545",
chainId: 31337,
},
// Add testnet configurations
sepolia: {
url: process.env.SEPOLIA_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 11155111,
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
gasReporter: {
enabled: process.env.REPORT_GAS !== undefined,
currency: "USD",
},
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts",
},
mocha: {
timeout: 40000,
},
};
// ===== contracts/Storage.sol =====
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Storage {
uint256 private number;
string private message;
address public owner;
event DataStored(uint256 indexed newValue, string indexed newMessage, address indexed storedBy);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor(uint256 _initialNumber, string memory _initialMessage) {
number = _initialNumber;
message = _initialMessage;
owner = msg.sender;
emit DataStored(_initialNumber, _initialMessage, msg.sender);
}
modifier onlyOwner() {
require(msg.sender == owner, "Storage: caller is not the owner");
_;
}
function store(uint256 _num, string memory _msg) public {
require(_num >= 0, "Storage: number must be non-negative");
require(bytes(_msg).length > 0, "Storage: message cannot be empty");
number = _num;
message = _msg;
emit DataStored(_num, _msg, msg.sender);
}
function retrieve() public view returns (uint256, string memory) {
return (number, message);
}
function increment() public {
number += 1;
emit DataStored(number, message, msg.sender);
}
function decrement() public {
require(number > 0, "Storage: number cannot be negative");
number -= 1;
emit DataStored(number, message, msg.sender);
}
function transferOwnership(address _newOwner) public onlyOwner {
require(_newOwner != address(0), "Storage: new owner is the zero address");
address previousOwner = owner;
owner = _newOwner;
emit OwnershipTransferred(previousOwner, _newOwner);
}
}
// ===== test/Storage.test.js =====
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Storage", function () {
let storage;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const Storage = await ethers.getContractFactory("Storage");
storage = await Storage.deploy(42, "Hello, Hardhat!");
await storage.deployed();
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await storage.owner()).to.equal(owner.address);
});
it("Should set the initial number and message", async function () {
const [number, message] = await storage.retrieve();
expect(number).to.equal(42);
expect(message).to.equal("Hello, Hardhat!");
});
it("Should emit DataStored event on deployment", async function () {
// Event is tested in deployment, so we just verify initial state
const [number, message] = await storage.retrieve();
expect(number).to.equal(42);
expect(message).to.equal("Hello, Hardhat!");
});
});
describe("Store function", function () {
it("Should store new number and message", async function () {
await storage.store(100, "New message");
const [number, message] = await storage.retrieve();
expect(number).to.equal(100);
expect(message).to.equal("New message");
});
it("Should emit DataStored event", async function () {
await expect(storage.store(100, "New message"))
.to.emit(storage, "DataStored")
.withArgs(100, "New message", owner.address);
});
it("Should reject negative numbers", async function () {
await expect(storage.store(-1, "Invalid"))
.to.be.revertedWith("Storage: number must be non-negative");
});
it("Should reject empty messages", async function () {
await expect(storage.store(100, ""))
.to.be.revertedWith("Storage: message cannot be empty");
});
});
describe("Increment and Decrement", function () {
it("Should increment the number", async function () {
await storage.increment();
const [number] = await storage.retrieve();
expect(number).to.equal(43);
});
it("Should decrement the number", async function () {
await storage.decrement();
const [number] = await storage.retrieve();
expect(number).to.equal(41);
});
it("Should reject decrement below zero", async function () {
// Deploy with zero initial value
const Storage = await ethers.getContractFactory("Storage");
const zeroStorage = await Storage.deploy(0, "Zero");
await zeroStorage.deployed();
await expect(zeroStorage.decrement())
.to.be.revertedWith("Storage: number cannot be negative");
});
});
describe("Ownership", function () {
it("Should transfer ownership", async function () {
await storage.transferOwnership(addr1.address);
expect(await storage.owner()).to.equal(addr1.address);
});
it("Should emit OwnershipTransferred event", async function () {
await expect(storage.transferOwnership(addr1.address))
.to.emit(storage, "OwnershipTransferred")
.withArgs(owner.address, addr1.address);
});
it("Should reject transfer to zero address", async function () {
await expect(storage.transferOwnership(ethers.constants.AddressZero))
.to.be.revertedWith("Storage: new owner is the zero address");
});
it("Should reject ownership transfer from non-owner", async function () {
await expect(storage.connect(addr1).transferOwnership(addr2.address))
.to.be.revertedWith("Storage: caller is not the owner");
});
});
});
// ===== scripts/deploy.js =====
const hre = require("hardhat");
async function main() {
console.log("Starting deployment...");
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
const balance = await deployer.getBalance();
console.log("Account balance:", hre.ethers.utils.formatEther(balance));
// Deploy Storage contract with initial values
const initialNumber = 0;
const initialMessage = "Welcome to Hardhat!";
const Storage = await hre.ethers.getContractFactory("Storage");
const storage = await Storage.deploy(initialNumber, initialMessage);
await storage.deployed();
console.log("Storage contract deployed to:", storage.address);
console.log("Transaction hash:", storage.deployTransaction.hash);
// Verify initial state
const [number, message] = await storage.retrieve();
console.log("Initial stored number:", number.toString());
console.log("Initial stored message:", message);
// Store some initial data
const tx = await storage.store(100, "Contract deployed successfully!");
await tx.wait();
console.log("Initial data stored");
// Save deployment info
const deploymentInfo = {
network: hre.network.name,
contract: "Storage",
address: storage.address,
deployer: deployer.address,
deployedAt: new Date().toISOString(),
initialNumber: number.toString(),
initialMessage: message,
};
// In a real project, save this to a file or database
console.log("Deployment info:", JSON.stringify(deploymentInfo, null, 2));
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
// ===== scripts/interact.js =====
const hre = require("hardhat");
async function main() {
console.log("Interacting with Storage contract...");
// Get contract address from environment or use hardcoded for demo
const contractAddress = process.env.CONTRACT_ADDRESS || "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const [signer] = await hre.ethers.getSigners();
console.log("Interacting with account:", signer.address);
// Connect to existing contract
const Storage = await hre.ethers.getContractFactory("Storage");
const storage = await Storage.attach(contractAddress);
try {
// Read current values
const [number, message] = await storage.retrieve();
console.log("\nCurrent stored values:");
console.log(" Number:", number.toString());
console.log(" Message:", message);
// Store new values
console.log("\nStoring new values...");
const tx = await storage.store(42, "Hello from interaction script!");
const receipt = await tx.wait();
console.log("Transaction confirmed in block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
// Read updated values
const [newNumber, newMessage] = await storage.retrieve();
console.log("\nUpdated stored values:");
console.log(" Number:", newNumber.toString());
console.log(" Message:", newMessage);
// Test increment
console.log("\nTesting increment...");
await storage.increment();
const [incNumber] = await storage.retrieve();
console.log("Number after increment:", incNumber.toString());
// Test decrement
console.log("\nTesting decrement...");
await storage.decrement();
const [decNumber] = await storage.retrieve();
console.log("Number after decrement:", decNumber.toString());
console.log("\nInteraction completed successfully!");
} catch (error) {
console.error("Error during interaction:", error);
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
// ===== package.json dependencies =====
{
"dependencies": {
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
"hardhat": "^2.17.0"
},
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-verify": "^1.0.0",
"@typechain/ethers-v6": "^0.4.0",
"@typechain/hardhat": "^8.0.0",
"chai": "^4.2.0",
"ethers": "^6.4.0",
"hardhat-gas-reporter": "^1.0.8",
"solidity-coverage": "^0.8.1",
"typechain": "^8.3.0"
}
}
// ===== Environment variables (.env example) =====
PRIVATE_KEY=your_private_key_here
SEPOLIA_URL=https://sepolia.infura.io/v3/your_infura_project_id
ETHERSCAN_API_KEY=your_etherscan_api_key
REPORT_GAS=true
// ===== Useful Hardhat commands =====
/*
npx hardhat compile # Compile contracts
npx hardhat test # Run tests
npx hardhat test --grep "Storage" # Run specific tests
npx hardhat node # Start local node
npx hardhat run scripts/deploy.js # Run deployment script
npx hardhat run scripts/interact.js --network localhost
npx hardhat verify CONTRACT_ADDRESS # Verify on Etherscan
npx hardhat coverage # Generate coverage report
npx hardhat clean # Clean cache and artifacts
*/
console.log("Hardhat Hello World project structure created!");
console.log("\nNext steps:");
console.log("1. Run 'npm install' to install dependencies");
console.log("2. Run 'npx hardhat compile' to compile contracts");
console.log("3. Run 'npx hardhat test' to run tests");
console.log("4. Run 'npx hardhat node' to start local development network");
console.log("5. Run 'npx hardhat run scripts/deploy.js --network localhost' to deploy");
console.log("6. Run 'npx hardhat run scripts/interact.js --network localhost' to interact");
💻 Tests Avancés Hardhat javascript
Patterns de tests avancés incluant les fixtures, les matchers personnalisés, la couverture, l'optimisation du gaz et les tests d'intégration
// Hardhat Advanced Testing Patterns
// Comprehensive testing strategies for smart contracts
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-network-helpers");
// ===== Test Fixtures =====
// test/fixtures.js
async function deployContractFixture() {
// Get signers
const [owner, addr1, addr2, addr3] = await ethers.getSigners();
// Deploy contract
const ContractFactory = await ethers.getContractFactory("YourContract");
const contract = await ContractFactory.deploy();
await contract.deployed();
// Fund other accounts
await owner.sendTransaction({
to: addr1.address,
value: ethers.utils.parseEther("10"),
});
await owner.sendTransaction({
to: addr2.address,
value: ethers.utils.parseEther("5"),
});
return { contract, owner, addr1, addr2, addr3 };
}
// ===== Custom Chai Matchers =====
// test/helpers/chai.js
const chai = require("chai");
const { ethers } = require("hardhat");
chai.use(function (chai, utils) {
// Matcher for checking if a transaction reverts with a specific message
chai.Assertion.addMethod('revertedWithMessage', function (expectedMessage) {
const promise = this._obj;
return promise
.then(() => {
throw new Error('Expected transaction to revert');
})
.catch((error) => {
if (error.message.includes('revert')) {
const revertMessage = error.message.match(/revert(.*)/);
const actualMessage = revertMessage ? revertMessage[1].trim() : '';
this.assert(
actualMessage.includes(expectedMessage),
`Expected transaction to revert with message containing "${expectedMessage}"`,
`Expected transaction to revert with message containing "${expectedMessage}", but got "${actualMessage}"`
);
} else {
throw error;
}
});
});
// Matcher for checking gas usage
chai.Assertion.addMethod('useGas', function (expectedGas) {
const receipt = this._obj;
const actualGas = receipt.gasUsed.toNumber();
this.assert(
actualGas <= expectedGas,
`Expected transaction to use at most ${expectedGas} gas, but used ${actualGas}`,
`Expected transaction to use at most ${expectedGas} gas, and used ${actualGas}`
);
});
// Matcher for checking ether balance changes
chai.Assertion.addMethod('changeEtherBalance', function (account, expectedChange) {
const promise = this._obj;
const provider = ethers.provider;
return getBalance(account)
.then((balanceBefore) => {
return promise.then((tx) => {
return tx.wait().then(() => {
return getBalance(account);
}).then((balanceAfter) => {
const change = balanceAfter.sub(balanceBefore);
this.assert(
change.eq(expectedChange),
`Expected ether balance change of ${ethers.utils.formatEther(expectedChange)} ETH`,
`Expected ether balance change of ${ethers.utils.formatEther(expectedChange)} ETH, but got ${ethers.utils.formatEther(change)} ETH`
);
});
});
});
async function getBalance(address) {
return await provider.getBalance(address);
}
});
});
// ===== Advanced Test Example =====
// test/YourContract.advanced.test.js
describe("YourContract - Advanced Tests", function () {
let contract, owner, addr1, addr2;
beforeEach(async function () {
({ contract, owner, addr1, addr2 } = await deployContractFixture());
});
describe("Gas Optimization Tests", function () {
it("Should track gas usage for different operations", async function () {
const tx1 = await contract.someFunction();
const receipt1 = await tx1.wait();
console.log(`Gas used for someFunction(): ${receipt1.gasUsed}`);
const tx2 = await contract.anotherFunction();
const receipt2 = await tx2.wait();
console.log(`Gas used for anotherFunction(): ${receipt2.gasUsed}`);
// Gas assertions
expect(receipt1).to.useGas(100000); // Adjust based on your contract
});
it("Should be more efficient with batch operations", async function () {
// Individual operations
const startGas = await ethers.provider.getBalance(owner.address);
await contract.individualOperation1();
await contract.individualOperation2();
await contract.individualOperation3();
const endGas = await ethers.provider.getBalance(owner.address);
const individualGasCost = startGas.sub(endGas);
// Batch operation
const startGasBatch = await ethers.provider.getBalance(owner.address);
await contract.batchOperation();
const endGasBatch = await ethers.provider.getBalance(owner.address);
const batchGasCost = startGasBatch.sub(endGasBatch);
console.log(`Individual operations cost: ${ethers.utils.formatEther(individualGasCost)} ETH`);
console.log(`Batch operation cost: ${ethers.utils.formatEther(batchGasCost)} ETH`);
expect(batchGasCost.lt(individualGasCost)).to.be.true;
});
});
describe("Time-Based Tests", function () {
it("Should handle time-locked functions correctly", async function () {
const lockTime = 7 * 24 * 60 * 60; // 7 days in seconds
// Lock some tokens
await contract.lockTokens(ethers.utils.parseEther("100"), lockTime);
// Try to withdraw before lock period
await expect(contract.withdrawLockedTokens())
.to.be.revertedWith("Tokens are still time-locked");
// Fast forward time
await time.increase(lockTime);
// Should succeed after lock period
await expect(contract.withdrawLockedTokens())
.to.emit(contract, "TokensWithdrawn");
});
it("Should calculate vesting schedules correctly", async function () {
const totalAmount = ethers.utils.parseEther("1000");
const vestingPeriod = 12 * 30 * 24 * 60 * 60; // 12 months
await contract.setVestingSchedule(totalAmount, vestingPeriod);
// Check vested amount after 6 months
await time.increase(vestingPeriod / 2);
const vestedAmount = await contract.getVestedAmount();
expect(vestedAmount).to.equal(totalAmount.div(2));
});
});
describe("State Transition Tests", function () {
it("Should handle complex state transitions", async function () {
// Initial state: Created
expect(await contract.getState()).to.equal(0); // Assuming 0 = Created
// Transition to Active
await contract.activate();
expect(await contract.getState()).to.equal(1); // Assuming 1 = Active
// Transition to Paused
await contract.pause();
expect(await contract.getState()).to.equal(2); // Assuming 2 = Paused
// Cannot perform certain actions while paused
await expect(contract.someRestrictedAction())
.to.be.revertedWith("Contract is paused");
// Resume
await contract.resume();
expect(await contract.getState()).to.equal(1); // Back to Active
});
});
describe("Edge Cases and Boundary Conditions", function () {
it("Should handle maximum values correctly", async function () {
const maxUint256 = ethers.constants.MaxUint256;
// Test with maximum values
await expect(contract.setSomeValue(maxUint256))
.to.not.be.reverted;
expect(await contract.getSomeValue()).to.equal(maxUint256);
});
it("Should handle zero address edge cases", async function () {
await expect(contract.transfer(ethers.constants.AddressZero, 100))
.to.be.revertedWith("Cannot transfer to zero address");
});
it("Should handle integer overflow/underflow", async function () {
// Test with Solidity 0.8+ which has built-in overflow protection
const balance = await contract.getBalance();
if (balance.gt(0)) {
await expect(contract.withdraw(balance.add(1)))
.to.be.reverted; // Should revert due to underflow
}
});
});
describe("Event-Driven Tests", function () {
it("Should emit correct events sequence", async function () {
const tx = await contract.complexOperation();
const receipt = await tx.wait();
// Check for multiple events in correct order
const events = receipt.events.filter(e => e.event === "StepCompleted");
expect(events.length).to.equal(3); // Assuming 3 steps
expect(events[0].args.step).to.equal(1);
expect(events[1].args.step).to.equal(2);
expect(events[2].args.step).to.equal(3);
});
});
describe("Integration Tests", function () {
it("Should work correctly with multiple contracts", async function () {
// Deploy dependent contracts
const Token = await ethers.getContractFactory("ERC20");
const token = await Token.deploy("Test Token", "TEST");
await token.deployed();
const Marketplace = await ethers.getContractFactory("Marketplace");
const marketplace = await Marketplace.deploy(token.address);
await marketplace.deployed();
// Setup
await token.transfer(marketplace.address, ethers.utils.parseEther("1000"));
// Test integration
const listingPrice = ethers.utils.parseEther("10");
await marketplace.createListing(listingPrice);
const listings = await marketplace.getListings();
expect(listings.length).to.equal(1);
expect(listings[0].price).to.equal(listingPrice);
});
});
describe("Fork Testing", function () {
it("Should interact with mainnet contracts", async function () {
// This test requires forking mainnet
// Add to hardhat.config.js:
// forking: {
// url: "https://eth-mainnet.alchemyapi.io/v2/YOUR-API-KEY",
// blockNumber: 15000000, // Optional: specific block
// }
const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const weth = await ethers.getContractAt("IERC20", WETH_ADDRESS);
const balance = await weth.balanceOf(owner.address);
console.log(`WETH balance: ${ethers.utils.formatEther(balance)}`);
expect(balance.gte(0)).to.be.true;
});
});
});
// ===== Property-Based Testing =====
// test/YourContract.property.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("YourContract - Property-Based Tests", function () {
let contract, owner;
beforeEach(async function () {
[owner] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("YourContract");
contract = await Contract.deploy();
await contract.deployed();
});
it("Should maintain invariant: totalSupply = sum of all balances", async function () {
// Transfer random amounts between accounts and check invariant
const accounts = await ethers.getSigners();
const initialSupply = await contract.totalSupply();
for (let i = 0; i < 100; i++) {
const fromIndex = Math.floor(Math.random() * 5);
const toIndex = Math.floor(Math.random() * 5);
const amount = ethers.BigNumber.from(Math.floor(Math.random() * 1000) + 1);
if (fromIndex !== toIndex) {
try {
await contract.connect(accounts[fromIndex]).transfer(accounts[toIndex].address, amount);
} catch (e) {
// Ignore failed transfers (insufficient balance)
}
}
// Check invariant
const currentSupply = await contract.totalSupply();
expect(currentSupply).to.equal(initialSupply);
}
});
});
// ===== Test Utilities =====
// test/utils/index.js
const { ethers } = require("hardhat");
// Helper functions for testing
const testHelpers = {
// Get timestamp of latest block
async getLatestBlockTimestamp() {
const block = await ethers.provider.getBlock("latest");
return block.timestamp;
},
// Calculate expected amount with fees
calculateAmountWithFee(amount, feePercent) {
return amount.mul(10000 - feePercent).div(10000);
},
// Generate random address
randomAddress() {
return ethers.Wallet.createRandom().address;
},
// Format wei to ether for display
formatEther(amount) {
return ethers.utils.formatEther(amount);
},
// Parse ether string to wei
parseEther(amount) {
return ethers.utils.parseEther(amount);
},
// Wait for specific number of blocks
async waitBlocks(count) {
for (let i = 0; i < count; i++) {
await ethers.provider.send("evm_mine", []);
}
},
// impersonate account (for fork testing)
async impersonateAccount(address) {
await ethers.provider.send("hardhat_impersonateAccount", [address]);
return await ethers.getSigner(address);
},
// Set balance for account
async setBalance(address, amount) {
await ethers.provider.send("hardhat_setBalance", [
address,
amount.toHexString(),
]);
},
};
module.exports = testHelpers;
// ===== Coverage Configuration =====
// hardhat.config.js (add to existing config)
module.exports = {
// ... existing config ...
solidity: {
// ... existing solidity config ...
},
mocha: {
timeout: 60000,
},
// Coverage configuration
coverage: {
IstanbulFolder: "coverage",
onCleanUp: "coverage",
reporter: ["html", "lcov", "text"],
threshold: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
exclude: [
"test/",
"scripts/",
"node_modules/",
],
},
};
console.log("Advanced testing patterns configured!");
console.log("\nRun these commands:");
console.log("npx hardhat test # Run all tests");
console.log("npx hardhat test --coverage # Run with coverage");
console.log("npx hardhat test --grep 'gas' # Run gas optimization tests");
console.log("npx hardhat test --network fork # Run fork tests");
💻 Plugins et Écosystème Hardhat javascript
Explorer les plugins Hardhat incluant les outils de sécurité, l'automatisation du déploiement, la vérification, les rapports de gaz et les utilitaires de développement
// Hardhat Plugins and Ecosystem Guide
// Comprehensive plugin configuration and usage examples
// ===== Enhanced hardhat.config.js with Plugins =====
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-verify");
require("hardhat-deploy");
require("hardhat-deploy-ethers");
require("hardhat-gas-reporter");
require("solidity-coverage");
require("@openzeppelin/hardhat-upgrades");
require("hardhat-contract-sizer");
require("hardhat-spdx-license-identifier");
require("hardhat-preprocessor");
require("@tenderly/hardhat-tenderly");
const fs = require("fs");
const path = require("path");
// Task for contract verification
task("verify-all", "Verifies all deployed contracts")
.addOptionalParam("network", "Network to verify on", "mainnet")
.setAction(async (taskArgs, { run }) => {
const deployments = fs.readFileSync(
path.join(__dirname, "deployments", taskArgs.network, ".deployments Manifest.json"),
"utf8"
);
const deploymentManifest = JSON.parse(deployments);
for (const [contractName, deployment] of Object.entries(deploymentManifest.implementation)) {
if (deployment.address && deployment.abi) {
console.log(`Verifying ${contractName} at ${deployment.address}`);
try {
await run("verify:verify", {
address: deployment.address,
constructorArguments: deployment.constructorArgs || [],
});
} catch (error) {
console.error(`Failed to verify ${contractName}:`, error.message);
}
}
}
});
// Task for gas optimization analysis
task("gas-analysis", "Analyzes gas usage across all contracts")
.setAction(async (_, { run }) => {
await run("compile");
await run("test", { reporter: "gas-reporter" });
});
// Task for security scan
task("security-scan", "Runs security analysis tools")
.setAction(async (_, { run }) => {
console.log("Running Slither analysis...");
const { execSync } = require("child_process");
try {
execSync("slither . --filter-paths 'node_modules/' --json slither-report.json", { stdio: 'inherit' });
console.log("Security scan completed. Check slither-report.json");
} catch (error) {
console.error("Security scan failed. Make sure Slither is installed.");
}
});
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: {
compilers: [
{
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
metadata: {
bytecodeHash: "none",
},
},
},
{
version: "0.6.12",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
],
},
defaultNetwork: "hardhat",
networks: {
hardhat: {
chainId: 31337,
forking: {
url: process.env.MAINNET_URL,
blockNumber: 16000000, // Optional specific block
},
},
localhost: {
url: "http://127.0.0.1:8545",
chainId: 31337,
},
// Testnet configurations
sepolia: {
url: process.env.SEPOLIA_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 11155111,
gasPrice: 20000000000, // 20 gwei
},
goerli: {
url: process.env.GOERLI_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 5,
},
// Mainnet
mainnet: {
url: process.env.MAINNET_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 1,
gasPrice: "auto",
gasMultiplier: 1.1,
},
},
// Named accounts for deploy scripts
namedAccounts: {
deployer: {
default: 0,
},
feeRecipient: {
default: 1,
},
},
// Gas reporter configuration
gasReporter: {
enabled: process.env.REPORT_GAS !== undefined,
currency: "USD",
gasPrice: 20,
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
excludeContracts: ["test/", "mock/"],
},
// Contract sizer
contractSizer: {
alphaSort: true,
disambiguatePaths: false,
runOnCompile: true,
strict: true,
},
// Etherscan verification
etherscan: {
apiKey: {
mainnet: process.env.ETHERSCAN_API_KEY,
sepolia: process.env.ETHERSCAN_API_KEY,
goerli: process.env.ETHERSCAN_API_KEY,
},
},
// Tenderly integration
tenderly: {
project: process.env.TENDERLY_PROJECT,
username: process.env.TENDERLY_USERNAME,
forkNetwork: "1", // Mainnet
},
// OpenZeppelin Upgrades
openzeppelin: {
version: "5.0.0",
},
// Preprocessor for imports
preprocess: {
eachLine: () => ({
transform: (line) => {
if (line.match(/^s*import ".*/./.*.sol";$/)) {
// Handle relative imports differently
return line;
}
return line;
},
}),
},
// Mocha configuration
mocha: {
timeout: 60000,
reporter: "mocha-multi-reporters",
reporterOptions: {
configFile: "mocha-reporter-config.json",
},
},
// Paths configuration
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts",
deploy: "./deploy",
deployments: "./deployments",
},
// SPDX license identifier
spdxLicenseIdentifier: "MIT",
};
// ===== Deployment Scripts with hardhat-deploy =====
// deploy/00-deploy-mocks.js
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
console.log("Deploying mock contracts with account:", deployer);
// Deploy ERC20 mock
await deploy("MockERC20", {
from: deployer,
args: ["Mock Token", "MOCK"],
log: true,
autoMine: true, // Speed up deployment on local network
});
// Deploy ERC721 mock
await deploy("MockERC721", {
from: deployer,
args: ["Mock NFT", "MNFT"],
log: true,
autoMine: true,
});
};
module.exports.tags = ["mocks", "test"];
// deploy/01-deploy-core.js
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
console.log("Deploying core contracts with account:", deployer);
// Get deployed mock contracts
const mockERC20 = await deployments.get("MockERC20");
// Deploy main contract
await deploy("MainContract", {
from: deployer,
args: [mockERC20.address],
log: true,
waitConfirmations: 1, // Wait for 1 confirmation on testnets/mainnets
});
// Verify on Etherscan (skip on localhost)
if (network.name !== "hardhat" && network.name !== "localhost") {
try {
await run("verify:verify", {
address: (await deployments.get("MainContract")).address,
constructorArguments: [mockERC20.address],
});
} catch (error) {
console.error("Verification failed:", error.message);
}
}
};
module.exports.tags = ["core"];
module.exports.dependencies = ["mocks"];
// deploy/02-deploy-upgradeable.js
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
console.log("Deploying upgradeable contracts with account:", deployer);
// Deploy upgradeable contract
await deploy("UpgradeableContract", {
from: deployer,
proxy: {
proxyContract: "OpenZeppelinTransparentProxy",
viaAdminContract: "DefaultProxyAdmin",
execute: {
init: {
methodName: "initialize",
args: ["Initial Value"],
},
},
},
log: true,
});
};
module.exports.tags = ["upgradeable"];
module.exports.dependencies = ["core"];
// ===== Security Testing Setup =====
// test/security/slither.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Security Tests", function () {
let contract, owner, attacker, user1, user2;
beforeEach(async function () {
[owner, attacker, user1, user2] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("YourContract");
contract = await Contract.deploy();
await contract.deployed();
});
// Reentrancy protection test
it("Should be protected against reentrancy attacks", async function () {
const MaliciousContract = await ethers.getContractFactory("ReentrancyAttacker");
const attacker = await MaliciousContract.deploy(contract.address);
await attacker.deployed();
await owner.sendTransaction({
to: contract.address,
value: ethers.utils.parseEther("10"),
});
// Attempt reentrancy attack
await expect(attacker.attack({ value: ethers.utils.parseEther("1") }))
.to.be.revertedWith("ReentrancyGuard: reentrant call");
});
// Integer overflow/underflow test
it("Should handle arithmetic safely", async function () {
const maxUint256 = ethers.constants.MaxUint256;
// Test overflow
await contract.setBalance(maxUint256);
await expect(contract.addToBalance(1))
.to.be.reverted; // Should revert with Solidity 0.8+ overflow protection
// Test underflow
await contract.setBalance(0);
await expect(contract.subtractFromBalance(1))
.to.be.reverted; // Should revert with underflow protection
});
// Access control test
it("Should enforce access controls properly", async function () {
const restrictedFunction = async () => {
return contract.restrictedFunction();
};
await expect(restrictedFunction()).to.be.revertedWith("Ownable: caller is not the owner");
await expect(contract.connect(attacker).restrictedFunction())
.to.be.revertedWith("Ownable: caller is not the owner");
// Owner should be able to call
await expect(contract.connect(owner).restrictedFunction())
.to.not.be.reverted;
});
// Front-running protection test
it("Should protect against front-running attacks", async function () {
const value = ethers.utils.parseEther("1");
// User submits transaction
const userTx = contract.connect(user1).submitTransaction(value, {
value: value,
});
// Attacker tries to front-run with higher gas price
const attackerTx = contract.connect(attacker).submitTransaction(value, {
value: value,
gasPrice: ethers.utils.parseUnits("100", "gwei"),
});
// Both transactions should be processed according to gas price
await expect(attackerTx).to.not.be.reverted;
await expect(userTx).to.not.be.reverted;
});
});
// ===== Gas Optimization Examples =====
// contracts/gas-optimized/Storage.sol
pragma solidity ^0.8.19;
contract GasOptimizedStorage {
// Use packed structs to save gas
struct UserInfo {
uint128 balance; // 128 bits is enough for most use cases
uint64 lastUpdate; // Unix timestamp fits in 64 bits
uint32 tier; // User tier fits in 32 bits
bool isActive; // 1 bit (but takes full byte)
}
mapping(address => UserInfo) public users;
// Use events instead of storage for historical data
event BalanceUpdated(address indexed user, uint256 newBalance, uint64 timestamp);
// Use immutable for deployment-time constants
uint256 public immutable MAX_BALANCE;
uint256 public immutable FEE_RATE;
constructor() {
MAX_BALANCE = 1000000 * 10**18; // 1 million tokens
FEE_RATE = 10; // 0.1%
}
// Optimized update function
function updateBalance(address user, uint128 newBalance) external {
UserInfo storage userInfo = users[user];
// Use unchecked where overflow is not possible
unchecked {
userInfo.balance = newBalance;
userInfo.lastUpdate = uint64(block.timestamp);
}
emit BalanceUpdated(user, newBalance, userInfo.lastUpdate);
}
// Batch operations to save gas
function batchUpdateBalances(
address[] calldata users_,
uint128[] calldata balances_
) external {
require(users_.length == balances_.length, "Array length mismatch");
uint256 length = users_.length;
for (uint256 i = 0; i < length;) {
updateBalance(users_[i], balances_[i]);
// Unchecked loop optimization
unchecked { ++i; }
}
}
}
// ===== Development Scripts =====
// scripts/dev-setup.js
const hre = require("hardhat");
const { ethers } = hre;
async function main() {
console.log("Setting up development environment...");
const [deployer] = await hre.ethers.getSigners();
console.log("Deployer account:", deployer.address);
// Deploy all contracts
await hre.run("deploy", {
tags: "all",
resetState: true,
});
// Fund test accounts
const accounts = await hre.ethers.getSigners();
for (let i = 1; i < 5; i++) {
await deployer.sendTransaction({
to: accounts[i].address,
value: ethers.utils.parseEther("100"),
});
}
console.log("Development environment setup complete!");
console.log("Test accounts funded with 100 ETH each");
}
// scripts/fork-test.js
const hre = require("hardhat");
async function main() {
console.log("Testing against forked mainnet...");
// Get mainnet WETH contract
const wethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const weth = await hre.ethers.getContractAt("IWETH", wethAddress);
const [signer] = await hre.ethers.getSigners();
const balance = await weth.balanceOf(signer.address);
console.log(`WETH balance on fork: ${hre.ethers.utils.formatEther(balance)} WETH`);
// Test interaction with mainnet contract
if (balance.eq(0)) {
const tx = await weth.deposit({
value: hre.ethers.utils.parseEther("1"),
});
await tx.wait();
console.log("Deposited 1 ETH for WETH");
}
}
// ===== Plugin-specific Configuration Files =====
// .mocharc.json
{
"require": "hardhat/register",
"timeout": 60000,
"reporter": "mocha-multi-reporters",
"reporter-options": {
"configFile": "mocha-reporter-config.json"
}
}
// mocha-reporter-config.json
{
"reporterEnabled": "spec,xunit,markdown",
"specReporterOutput": "test-results.txt",
"xunitReporterOutput": "test-results.xml",
"markdownReporterOutput": "test-results.md"
}
// .solhint.json
{
"extends": "solhint:recommended",
"plugins": [],
"rules": {
"compiler-version": ["error", "^0.8.0"],
"func-visibility": ["warn", { "ignoreConstructors": true }],
"var-name-mixedcase": "error",
"const-name-snakecase": "error",
"contract-name-camelcase": "error",
"event-name-camelcase": "error",
"struct-name-camelcase": "error",
"modifier-name-mixedcase": "error",
"private-vars-leading-underscore": "error",
"use-forbidden-name": "error"
}
}
console.log("Hardhat plugins ecosystem configured!");
console.log("\nAvailable plugins and features:");
console.log("✅ @nomicfoundation/hardhat-toolbox - All-in-one development suite");
console.log("✅ hardhat-deploy - Declarative deployment system");
console.log("✅ hardhat-gas-reporter - Gas usage tracking");
console.log("✅ hardhat-contract-sizer - Contract size monitoring");
console.log("✅ @openzeppelin/hardhat-upgrades - Upgradeable contracts");
console.log("✅ solid-coverage - Code coverage analysis");
console.log("✅ @tenderly/hardhat-tenderly - Forking and debugging");
console.log("\nCustom tasks available:");
console.log("npx hardhat verify-all");
console.log("npx hardhat gas-analysis");
console.log("npx hardhat security-scan");
💻 Contrats Améliorables Hardhat javascript
Construire et déployer des contrats intelligents améliorables en utilisant les proxies OpenZeppelin, les patterns d'implémentation et les stratégies d'amélioration
// Hardhat Upgradeable Contracts Guide
// OpenZeppelin Proxy Contracts and Upgrade Patterns
// ===== Upgradeable Contract Examples =====
// contracts/upgradeable/BoxV1.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract BoxV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 private _value;
event ValueChanged(uint256 indexed newValue, address indexed changedBy);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
// Prevents initialization of implementation contract
_disableInitializers();
}
function initialize(uint256 initialValue) public initializer {
__Ownable_init();
__UUPSUpgradeable_init();
_value = initialValue;
emit ValueChanged(initialValue, msg.sender);
}
function store(uint256 newValue) public {
require(newValue >= 0, "Box: value must be non-negative");
_value = newValue;
emit ValueChanged(newValue, msg.sender);
}
function retrieve() public view returns (uint256) {
return _value;
}
function version() public pure returns (uint256) {
return 1;
}
// Required by UUPS upgradeability
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
// contracts/upgradeable/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract BoxV2 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 private _value;
string private _name;
uint256 private _lastUpdated;
event ValueChanged(uint256 indexed newValue, address indexed changedBy);
event NameChanged(string indexed newName, address indexed changedBy);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(uint256 initialValue, string memory initialName) public initializer {
__Ownable_init();
__UUPSUpgradeable_init();
_value = initialValue;
_name = initialName;
_lastUpdated = block.timestamp;
emit ValueChanged(initialValue, msg.sender);
emit NameChanged(initialName, msg.sender);
}
function store(uint256 newValue) public {
require(newValue >= 0, "Box: value must be non-negative");
_value = newValue;
_lastUpdated = block.timestamp;
emit ValueChanged(newValue, msg.sender);
}
function retrieve() public view returns (uint256) {
return _value;
}
function name() public view returns (string memory) {
return _name;
}
function setName(string memory newName) public onlyOwner {
_name = newName;
_lastUpdated = block.timestamp;
emit NameChanged(newName, msg.sender);
}
function lastUpdated() public view returns (uint256) {
return _lastUpdated;
}
function version() public pure returns (uint256) {
return 2;
}
// New functionality in V2
function increment() public {
_value += 1;
_lastUpdated = block.timestamp;
emit ValueChanged(_value, msg.sender);
}
function decrement() public {
require(_value > 0, "Box: value cannot be negative");
_value -= 1;
_lastUpdated = block.timestamp;
emit ValueChanged(_value, msg.sender);
}
// Required by UUPS upgradeability
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
// ===== Deployment Scripts =====
// deploy/01-deploy-box-v1.js
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, save } = deployments;
const { deployer } = await getNamedAccounts();
console.log("Deploying BoxV1 with account:", deployer);
// Deploy BoxV1 as upgradeable proxy
const boxDeployment = await deploy("BoxV1", {
from: deployer,
args: [42], // initialValue
proxy: {
proxyContract: "UUPSProxy",
implementation: "BoxV1",
methodName: "initialize",
},
log: true,
waitConfirmations: 1,
});
console.log("BoxV1 deployed at:", boxDeployment.address);
// Save deployment info
await save("BoxProxy", {
address: boxDeployment.address,
abi: (await deployments.getArtifact("BoxV1")).abi,
linkedData: {
type: "UUPSProxy",
implementation: "BoxV1",
},
});
// Test the deployment
const Box = await ethers.getContractFactory("BoxV1");
const box = Box.attach(boxDeployment.address);
const value = await box.retrieve();
const version = await box.version();
console.log("Initial value:", value.toString());
console.log("Version:", version.toString());
expect(value).to.equal(42);
expect(version).to.equal(1);
};
module.exports.tags = ["BoxV1"];
// deploy/02-upgrade-box-v2.js
module.exports = async ({ getNamedAccounts, deployments }) => {
const { execute, read, save } = deployments;
const { deployer } = await getNamedAccounts();
console.log("Upgrading Box to V2 with account:", deployer);
// Get current proxy address
const proxyAddress = (await deployments.get("BoxProxy")).address;
console.log("Proxy address:", proxyAddress);
// Upgrade to BoxV2
const upgradeTx = await execute(
"BoxV1", // Using BoxV1 as the proxy admin
{ from: deployer },
"upgradeTo",
(await deployments.getArtifact("BoxV2")).address
);
console.log("Upgrade transaction hash:", upgradeTx.hash);
await upgradeTx.wait();
// Update saved deployment info
await save("BoxProxy", {
address: proxyAddress,
abi: (await deployments.getArtifact("BoxV2")).abi,
linkedData: {
type: "UUPSProxy",
implementation: "BoxV2",
},
});
// Test the upgrade
const BoxV2 = await ethers.getContractFactory("BoxV2");
const box = BoxV2.attach(proxyAddress);
const value = await box.retrieve();
const version = await box.version();
console.log("Value after upgrade:", value.toString());
console.log("Version after upgrade:", version.toString());
expect(value).to.equal(42); // Value should be preserved
expect(version).to.equal(2); // Version should be updated
// Test new functionality
await box.increment();
const newValue = await box.retrieve();
console.log("Value after increment:", newValue.toString());
expect(newValue).to.equal(43);
// Test setName function
await box.setName("My Upgraded Box");
const name = await box.name();
console.log("Box name:", name);
expect(name).to.equal("My Upgraded Box");
};
module.exports.tags = ["BoxV2"];
module.exports.dependencies = ["BoxV1"];
// ===== Testing Upgradeable Contracts =====
// test/upgradeable/Box.test.js
const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");
describe("Box Upgradeable Contract", function () {
let box;
let boxV2Factory;
let owner, addr1, addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
// Deploy initial version
const Box = await ethers.getContractFactory("BoxV1");
box = await upgrades.deployProxy(Box, [42], { initializer: "initialize" });
await box.deployed();
// Get V2 factory for upgrade tests
boxV2Factory = await ethers.getContractFactory("BoxV2");
});
describe("Initial Deployment (V1)", function () {
it("Should set the right initial value", async function () {
expect(await box.retrieve()).to.equal(42);
});
it("Should return version 1", async function () {
expect(await box.version()).to.equal(1);
});
it("Should allow storing new values", async function () {
await box.store(100);
expect(await box.retrieve()).to.equal(100);
});
it("Should reject negative values", async function () {
await expect(box.store(-1)).to.be.revertedWith("Box: value must be non-negative");
});
});
describe("Proxy Behavior", function () {
it("Should have the correct owner", async function () {
expect(await box.owner()).to.equal(owner.address);
});
it("Should emit events on value changes", async function () {
await expect(box.store(100))
.to.emit(box, "ValueChanged")
.withArgs(100, owner.address);
});
it("Should prevent initialization twice", async function () {
await expect(box.initialize(50))
.to.be.revertedWith("Initializable: contract is already initialized");
});
});
describe("Upgrade to V2", function () {
let upgradedBox;
beforeEach(async function () {
// Upgrade to V2
upgradedBox = await upgrades.upgradeProxy(box, boxV2Factory);
// Note: upgradedBox and box point to the same proxy
});
it("Should preserve existing state", async function () {
expect(await upgradedBox.retrieve()).to.equal(42);
expect(await upgradedBox.owner()).to.equal(owner.address);
});
it("Should return new version", async function () {
expect(await upgradedBox.version()).to.equal(2);
});
it("Should have new functionality", async function () {
await upgradedBox.increment();
expect(await upgradedBox.retrieve()).to.equal(43);
await upgradedBox.decrement();
expect(await upgradedBox.retrieve()).to.equal(42);
});
it("Should prevent invalid decrement", async function () {
await upgradedBox.store(0);
await expect(upgradedBox.decrement())
.to.be.revertedWith("Box: value cannot be negative");
});
it("Should support name functionality", async function () {
await upgradedBox.setName("Test Box");
expect(await upgradedBox.name()).to.equal("Test Box");
});
it("Should emit name change events", async function () {
await expect(upgradedBox.setName("New Name"))
.to.emit(upgradedBox, "NameChanged")
.withArgs("New Name", owner.address);
});
it("Should restrict name changes to owner", async function () {
await expect(upgradedBox.connect(addr1).setName("Hacker Name"))
.to.be.revertedWith("Ownable: caller is not the owner");
});
});
describe("Storage Layout Compatibility", function () {
it("Should maintain storage layout compatibility", async function () {
// Store some data before upgrade
await box.store(123);
// Upgrade
const upgradedBox = await upgrades.upgradeProxy(box, boxV2Factory);
// Data should still be accessible
expect(await upgradedBox.retrieve()).to.equal(123);
// New storage should work
await upgradedBox.setName("Storage Test");
expect(await upgradedBox.name()).to.equal("Storage Test");
});
});
describe("Access Control Through Upgrade", function () {
it("Should maintain access control after upgrade", async function () {
// Transfer ownership before upgrade
await box.transferOwnership(addr1.address);
expect(await box.owner()).to.equal(addr1.address);
// Upgrade
const upgradedBox = await upgrades.upgradeProxy(box, boxV2Factory);
// Ownership should be preserved
expect(await upgradedBox.owner()).to.equal(addr1.address);
// New owner should be able to call new functions
await expect(upgradedBox.connect(addr1).setName("Owner Can Change"))
.to.not.be.reverted;
// Old owner should not be able to call protected functions
await expect(upgradedBox.setName("Old Owner Cannot Change"))
.to.be.revertedWith("Ownable: caller is not the owner");
});
});
});
// ===== Advanced Upgrade Patterns =====
// contracts/upgradeable/StorageManager.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
contract StorageManager is Initializable, UUPSUpgradeable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
mapping(bytes32 => bytes32) private _storage;
mapping(bytes32 => bool) private _permissions;
mapping(address => uint256) private _userNonces;
event StorageSet(bytes32 indexed key, bytes32 indexed value, address indexed setBy);
event StorageDeleted(bytes32 indexed key, address indexed deletedBy);
event PermissionSet(bytes32 indexed key, bool allowed, address indexed setBy);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address admin) public initializer {
__AccessControl_init();
__UUPSUpgradeable_init();
__ReentrancyGuard_init();
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(UPGRADER_ROLE, admin);
_grantRole(MANAGER_ROLE, admin);
}
function setStorage(bytes32 key, bytes32 value) external nonReverter onlyRole(MANAGER_ROLE) {
require(_permissions[key] || msg.sender == getRoleMember(DEFAULT_ADMIN_ROLE, 0),
"StorageManager: no permission for key");
_storage[key] = value;
emit StorageSet(key, value, msg.sender);
}
function getStorage(bytes32 key) external view returns (bytes32) {
return _storage[key];
}
function deleteStorage(bytes32 key) external onlyRole(MANAGER_ROLE) {
delete _storage[key];
emit StorageDeleted(key, msg.sender);
}
function setPermission(bytes32 key, bool allowed) external onlyRole(DEFAULT_ADMIN_ROLE) {
_permissions[key] = allowed;
emit PermissionSet(key, allowed, msg.sender);
}
function hasPermission(bytes32 key) external view returns (bool) {
return _permissions[key];
}
// Batch operations for gas efficiency
function batchSetStorage(bytes32[] calldata keys, bytes32[] calldata values)
external nonReverter onlyRole(MANAGER_ROLE) {
require(keys.length == values.length, "StorageManager: array length mismatch");
for (uint256 i = 0; i < keys.length; i++) {
require(_permissions[keys[i]] || msg.sender == getRoleMember(DEFAULT_ADMIN_ROLE, 0),
"StorageManager: no permission for key");
_storage[keys[i]] = values[i];
emit StorageSet(keys[i], values[i], msg.sender);
}
}
function getUserNonce(address user) external view returns (uint256) {
return _userNonces[user];
}
function version() public pure returns (uint256) {
return 1;
}
function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {}
}
// ===== Upgrade Validation Script =====
// scripts/validate-upgrade.js
const { ethers, upgrades } = require("hardhat");
const fs = require("fs");
const path = require("path");
async function validateUpgrade(proxyAddress, newImplementationFactory) {
console.log("Validating upgrade safety...");
try {
// Check if upgrade is safe
const BoxV1 = await ethers.getContractFactory("BoxV1");
const box = BoxV1.attach(proxyAddress);
// Validate upgrade
const upgradeValidation = await upgrades.validateUpgrade(proxyAddress, newImplementationFactory);
console.log("✅ Upgrade validation passed");
// Check storage layout
const storageLayout = await newImplementationFactory.getStorageLayout();
console.log("Storage layout:", JSON.stringify(storageLayout, null, 2));
// Simulate upgrade
const upgradedBox = await upgrades.upgradeProxy(box, newImplementationFactory, {
unsafeAllowStorage: false, // Set to true only if you're sure about storage compatibility
});
console.log("✅ Upgrade simulation successful");
console.log("New implementation address:", upgradedBox.address);
return true;
} catch (error) {
console.error("❌ Upgrade validation failed:", error.message);
return false;
}
}
// Automated upgrade testing
async function testUpgrade() {
const proxyAddress = "YOUR_PROXY_ADDRESS_HERE"; // Get from deployments
// Test V1 to V2 upgrade
const BoxV2 = await ethers.getContractFactory("BoxV2");
const isValid = await validateUpgrade(proxyAddress, BoxV2);
if (isValid) {
console.log("✅ Upgrade from V1 to V2 is safe");
} else {
console.log("❌ Upgrade from V1 to V2 is NOT safe");
}
}
// ===== Production Upgrade Guide =====
const productionUpgradeChecklist = `
🔍 PRODUCTION UPGRADE CHECKLIST
1. PRE-UPGRADE PREPARATIONS:
[ ] Comprehensive testing on testnets
[ ] Security audit completed
[ ] Storage layout compatibility verified
[ ] Upgrade simulation successful
[ ] Rollback plan prepared
[ ] Community/teams notified
2. UPGRADE EXECUTION:
[ ] Backup current state
[ ] Deploy new implementation
[ ] Validate upgrade function
[ ] Execute upgrade with proper timelock
[ ] Monitor upgrade transaction
3. POST-UPGRADE VERIFICATION:
[ ] All functionality working
[ ] Data integrity preserved
[ ] Events emitted correctly
[ ] Gas consumption as expected
[ ] No unexpected side effects
4. MONITORING:
[ ] Set up alerts for anomalies
[ ] Monitor contract interactions
[ ] Track gas usage patterns
[ ] Watch for reverts or failures
[ ] Log all upgrade activities
⚠️ UPGRADE SECURITY CONSIDERATIONS:
- Use multisig or DAO for upgrade authorization
- Implement timelock for upgrade delays
- Consider using upgrade guardians
- Document all upgrade reasons
- Maintain transparent communication
`;
console.log("Hardhat upgradeable contracts guide configured!");
console.log("\nKey concepts:");
console.log("✅ Proxy patterns (UUPS, Transparent, Beacon)");
console.log("✅ Storage layout compatibility");
console.log("✅ Initialization patterns");
console.log("✅ Access control through upgrades");
console.log("✅ Upgrade validation and testing");
console.log("✅ Production upgrade procedures");
console.log("\nCommands:");
console.log("npx hardhat deploy --tags BoxV1");
console.log("npx hardhat deploy --tags BoxV2");
console.log("npx hardhat test test/upgradeable/Box.test.js");