🎯 Рекомендуемые коллекции
Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать
Примеры Hardhat Development Framework
Примеры среды разработки Ethereum Hardhat включая умные контракты, тестирование, развертывание, скрипты, плагины и рабочие процессы разработки
💻 Hardhat Hello World - Базовая настройка javascript
Полная настройка проекта Hardhat с умными контрактами, тестами, скриптами развертывания и конфигурацией
// 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");
💻 Продвинутое тестирование Hardhat javascript
Продвинутые паттерны тестирования включая фикстуры, кастомные матчеры, покрытие, оптимизацию газа и интеграционные тесты
// 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");
💻 Плагины и экосистема Hardhat javascript
Исследование плагинов Hardhat включая инструменты безопасности, автоматизацию развертывания, верификацию, отчеты о газе и утилиты разработки
// 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");
💻 Обновляемые контракты Hardhat javascript
Создание и развертывание обновляемых умных контрактов с использованием прокси OpenZeppelin, паттернов реализации и стратегий обновления
// 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");