7. Hardhat | Fund Me
Linting
solhint
is a linter for solidity which will analyze code for potential errors, checks for best practices of code and does formatting
Hardhat Deploy
hardhat-deploy
is an npm package which automates the process of writing deploy scripts for our contractsNeeds to be overriden as per github instructions
Add a
require
statement inhardhat.config.js
Make a
deploy
folder in the project which will contain deployment configuration
deploy/01-deploy-fund-me.js
// Method 1
// function deployFunc() {
// console.log("Hi!")
// }
// module.exports.default = deployFunc
// Method 2
// module.exports = async (hre) => {
// // hre.getNamedAccounts, hre.deployments
// const { getNamedAccounts, deployments } = hre
// }
// Method 3
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, log } = deployments
const { deployer } = await getNamedAccounts()
const chainId = network.config.chainId
}
Mocking & helper-hardhat-config
Mocking which is used in unit testing is creating objects that simulate behaviour of real objects
We want to use mock when using localhost or hardhat network for the price converter contract
helper-hardhat-config is a javascript file taken from
aave
repository which deploys their code to multiple different chains and multiple different addresses, it will help us get priceFeed address (of chainlink) for the chain that we usesWhen we run a local hardhat node, it will automatically deploy all the contracts
contracts/FundMe.sol
Added priceFeedAddress
in constructor for dynamic chain eth/usd mapping
// ...
contract FundMe {
// ...
AggregatorV3Interface public priceFeed;
constructor(address priceFeedAddress) {
i_owner = msg.sender;
priceFeed = AggregatorV3Interface(priceFeedAddress);
}
function fund() public payable {
require(
msg.value.getConversionRate(priceFeed) >= MINIMUM_USD,
"Didn't send enough!"
);
funders.push(msg.sender);
addressToAmountFunded[msg.sender] = msg.value;
}
// ...
}
contracts/PriceConverter.sol
Adjusted this contract as per FundMe contract to accept dynamic priceFeed
contract address
// ...
library PriceConverter {
function getPrice(AggregatorV3Interface priceFeed)
internal
view
returns (uint256)
{
(, int256 price, , , ) = priceFeed.latestRoundData();
return uint256(price * 1e10);
}
function getConversionRate(
uint256 ethAmount,
AggregatorV3Interface priceFeed
) internal view returns (uint256) {
uint256 ethPrice = getPrice(priceFeed);
uint256 ethAmountInUsd = (ethPrice * ethAmount);
ethAmountInUsd /= 1e18;
return ethAmountInUsd;
}
}
contracts/test/MockV3Aggregator.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0; // We can add different solidity compiler versions in hardhat config
// Importing directly the mock contract from chainlink repo
import "@chainlink/contracts/src/v0.6/tests/MockV3Aggregator.sol";
helper-hardhat-config.js
const networkConfig = {
5: {
name: "goerli",
ethUsdPriceFeed: "0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e"
},
137: {
name: "polygon",
ethUsdPriceFeed: "0xF9680D99D6C9589e2a93a78A04A279e509205945"
}
}
const developmentChains = ["hardhat", "localhost"]
const DECIMALS = "8"
const INITIAL_PRICE = "200000000000"
module.exports = {
networkConfig,
developmentChains,
DECIMALS,
INITIAL_PRICE,
}
deploy/00-deploy-mocks.js
const { network } = require("hardhat")
const { developmentChains, DECIMALS, INITIAL_PRICE } = require("../helper-hardhat-config")
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, log } = deployments
const { deployer } = await getNamedAccounts()
const chainId = network.config.chainId
if (developmentChains.includes(network.name)) {
log("Local network detected! Deploying mocks...")
await deploy("MockV3Aggregator", {
contract: "MockV3Aggregator",
from: deployer,
log: true,
args: [DECIMALS, INITIAL_PRICE], // Required arguments in mock contract as per github code
})
log("Mocks deployed!")
log("--------------------------------------------------")
}
}
// run only when we specify these tags in deploy command i.e `yarn hardhat deploy --tags mocks`
module.exports.tags = ["all", "mocks"]
01-deploy-fund-me.js
const { networkConfig, developmentChains } = require("../helper-hardhat-config")
const { network } = require("hardhat")
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, log, get } = deployments
const { deployer } = await getNamedAccounts()
const chainId = network.config.chainId
// if chainId is X use address Y
// if chainId is Z use address A
let ethUsdPriceFeedAddress
if (developmentChains.includes(network.name)) {
const ethUsdAggregator = await get("MockV3Aggregator")
ethUsdPriceFeedAddress = ethUsdAggregator.address
} else {
ethUsdPriceFeedAddress = networkConfig[chainId]["ethUsdPriceFeed"];
}
// if the contract doesn't exist, we deploy a minimal version of contract for our testing
// what happens when we want to change chains
// when going for localhost or hardhat network, we want to use a mock
const fundMe = await deploy("FundMe", {
from: deployer,
args: [ethUsdPriceFeedAddress], // put price feed address
log: true
})
log("-------------------------------------------------------")
}
module.exports.tags = ["all", "fundme"]
Utils Folder
We put commonly used scripts in this folder like the verify contract so that every one uses it (OOP)
utils/verify.js
const { run } = require("hardhat")
const verify = async (contractAddress, args) => {
console.log("Verifying contract...")
try {
await run("verify:verify", {
address: contractAddress,
constructorArguments: args
})
} catch (e) {
if (e.message.toLowerCase().includes("already verified")) {
console.log("Already Verified!")
} else {
console.log(e)
}
}
}
module.exports = { verify }
deploy/01-deploy-fund-me.js
// ...
const args = [ethUsdPriceFeedAddress]
const fundMe = await deploy("FundMe", {
from: deployer,
args: args, // put price feed address
log: true,
waitConfirmations: network.config.blockConfirmations || 1,
})
if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
// verify the contracts
await verify(fundMe.address, args)
}
log("-------------------------------------------------------")
Solidity Style Guide
We are going to follow solidity docs official style guide to make our code much neater and readable by following orders and conventions
We use
Natspec
format to document our codeUsed later on to automatically generate code documentation for other developeres
Test FundMe
We make seperate tests for
staging
&unit
In unit tests, individual components are tested
They are done
locally
on local hardhat network
fored hardhat network
Similar to SimpleStorage tests written in previous notes section
In staging or integration tests, whole flow is tested, usually done before deploying to production
They are done on a
testnet
Debugging & Breakpoints
We can pin a breakpoint in vscode & use
javascript debug terminal
to debug the code and see variable values on the breakpoints
Solidity-Console-Log
In solidity, we can import
hardhat/console.sol;
and useconsole.log()
inside the solidity code to debug the contract
Storage in Solidity
Each global/storage variable is stored seperately in the storage
Each slot is 32 bytes long and represents the bytes version of the object
e.g, uint256 25 is 0x000...0019 since thats the hex representation
For aa "true" boolean, it would be 0x000...001 since thats its hex
For dynamic values like mappings and dynamic arrays, the elements are stored using a hashing function. More info in documentation
For arrays, a sequential storage spot is taken up for the length of the array
For mappings, a sequantial storage spot is taken up, but left blank
Constants and immutable variables are not stored in storage, but they are considered part of the core of the bytecode of the contract
getStorageAt
We can query smart contracts to find out values of stored variables at a particular slot
log("logging storage")
for (let i = 0; i < 10; i++) {
log(
`Location ${i}: ${await ethers.provider.getStorageAt(
fundMe.address,
i
)}`
)
}
Gas & OpCodes
When we compile our contracts, in our bytecode object, there are opcodes present. Each opCode represent an operation in the contract
Each opCode has a gas price associated with it
SLOAD
opcode is used to load word from storage and it costs 800 gasSSTORE
opcode is used to save word to storage and it costs 20000** gasTherefore, in our contracts, we prepend
s_
keyword with variable names to denote that they are storage varaibles and will cost a lot of gas
Solidity Chainlink Style Guide
For normal contract users, we should make the
s_<variables>
private and make naming friendly getter functions for them.
Reference Code
https://github.com/Noman-Aziz/Hardhat-101-FundMe
Last updated