- Foundry totally written on solidity.
Note : dependencies are added as git-submodules and not as npm or nodejs modules
-
src folder : All our main smart contracts
-
test folder : All the test are written here.
-
scripts folder : To interact with smart contract we will write scripting file in soilidity
-
Project is configured using the foundry.toml file
-
lib folder : Dependencies are stored as git-submodules in lib/
-
After compiling/deploying the smart contract abi array will be in out/ folder in contract name file
// only once
curl -L https://foundry.paradigm.xyz | bash
source ~/.bashrc
foundryup
// to initialize project
forge init ProjectName
forge install openzeppelin/openzeppelin-contracts
forge install smartcontractkit/chainlink-brownie-contracts --no-commit
forge install transmissions11/solmate
forge install Cyfrin/foundry-devops --no-commit
forge : the build, test, debug, deploy smart contracts anvil : the foundry equivalent of Ganache cast : low level access to smart contracts (a bit of a truffle console equivalent)
forge build
// .env
SEPOLIA_RPC_URL=
PRIVATE_KEY=
ETHERSCAN_API_KEY=
// foundry.toml
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
[etherscan]
sepolia = { key = "${ETHERSCAN_API_KEY}"}
// This loads in the private key from our .env file
uint256 privateKey = vm.envUint("ANVIL_PRIVATE_KEY");
-
Written in solidity
-
they are run on the fast Foundry EVM backend, which provides dry-run capabilities.
-
By default, scripts are executed by calling the function named
run
, our entrypoint. -
Pass all the constructor params in contract instance.
-
We will use
HelperConfig.s.sol and Intraction.s.sol
file in ourDeploy.s.sol
// Scripting with Arguments(Passing params from command line) OPTIONAL
forge script --chain sepolia script/Deploy.s.sol:MyScript "NFT tutorial" TUT baseUri --sig 'run(string,string,string)' --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv
// using anvil
anvil
forge script script/Deploy.s.sol:MyScript --fork-url http://localhost:8545 --broadcast
forge script script/Deploy.s.sol:MyScript --fork-url http://localhost:8545 --account <account_name> --sender <address> --broadcast
// on testnet sepolia
forge script script/Deploy.s.sol:MyScript --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv
forge script script/Deploy.s.sol:MyScript --rpc-url $SEPOLIA_RPC_URL --account <account_name> --sender <address> --broadcast --verify -vvvv
forge script script/Deploy.s.sol:MyScript --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --broadcast --verify -vvvv
- Here, we will not store our private key in dotenv file. Rather, we will store it in KeyStore provided by foundry.
- Once we have stored it in keystore we can used it in any project. Note : This is useful when we need to submit our private key in an terminal.
cast wallet import privateKey --interactive
cast wallet list
- deploy our Smart Contract using Foundry scripts.
- We will write the deploy code in the script folder in solidity.
By default, scripts are executed by calling the function named run, our entrypoint.
script/Deploy.s.sol
import {Script} from "forge-std/Script.sol";
import {TestContract} from "../src/Web3.sol";
contract MyScript is Script{
// BY DEFAULT forge script EXECUTES THE 'run' FUNCTION DURING DEPLOYMENT
function setUp() external returns(TestContract){
// This loads in the private key from our .env file
uint256 privateKey = vm.envUint("ANVIL_PRIVATE_KEY");
// contract creations made by our main script contract.
// private key is passed to instruct to use that key for signing the transactions.
vm.startBroadcast(privateKey);
// If we have constructor then passed the value in the function as params.
// CREATED A NEW CONTRACT INSTANCE.
TestContract token = new TestContract("Token Name","ETH", "base_URL");
vm.stopBroadcast();
return token;
}
function run() external returns(TestContract){
return setUp();
}
}
- In
HelperConfig.s.sol
file we will declare all theparams, function and variables
we need to pass in constructor during deployment.
HelperConfig.s.sol
contract HelperConfig is Script{
// ERROR
error HelperConfig__InvalidChainId();
// TYPES (pass all the constructor params here)
struct NetworkConfig {
uint priceFeed;
}
// STATE VARIABLES
// Local network state variables
NetworkConfig public localNetworkConfig;
mapping(uint256 chainId => NetworkConfig) public networkConfigs;
// FUNCTIONS
constructor(){
networkConfigs[ETH_SEPOLIA_CHAIN_ID] = getSepoliaETHConfig();
networkConfigs[ZKSYNC_SEPOLIA_CHAIN_ID] = getL2ChainConfig();
networkConfigs[LOCAL_CHAIN_ID] = getAnvilETHConfig();
}
function getConfig() public view returns(NetworkConfig memory){
return getConfigByChainId(block.chainid);
}
function getConfigByChainId(uint256 chainId) public view returns(NetworkConfig memory){
if(networkConfigs[chainId].VRFCoordinator != address(0)){
return networkConfigs[chainId];
} else if(chainId == LOCAL_CHAIN_ID){
return networkConfigs[chainId];
}else{
revert HelperConfig__InvalidChainId();
}
}
// CONFIGS FOR SEPOLIA AND L2 CHAINS
function getSepoliaETHConfig() public pure returns(NetworkConfig memory){
return NetworkConfig({priceFeed:200});
}
function getL2ChainConfig() public view returns(NetworkConfig memory){
return NetworkConfig({priceFeed:200});
}
// LOCAL CONFIG (Local testing using a Mock contract)
// Here, we will write the mock script smart contract on local network
function getAnvilETHConfig() public returns(NetworkConfig memory){
// Check to see if we set an active network config
if(localNetworkConfig.VRFCoordinator != address(0)){
return localNetworkConfig;
}
// DEPLOY MOCK SMART CONTRACT
vm.startBroadcast();
VRFCoordinatorV2_5Mock mockVRFcontract = new VRFCoordinatorV2_5Mock(MOCK_BASEPRICE);
vm.stopBroadcast();
localNetworkConfig = NetworkConfig({priceFeed:200});
return localNetworkConfig;
}
}
- In
Interaction.s.sol
we will create functions from which ouron-chain data interacts with off-chain data
- Example : chainlink VRF, chainlink automation, Data feeds and chainlink functions.
Interaction.s.sol
import {Lottery} from "src/Lottery.sol";
import {HelperConfig, CodeConstants} from "./HelperConfig.s.sol";
contract FundSubscription is Script{
function fundSubscriptionWithConfig() public {
HelperConfig helperConfig = new HelperConfig();
uint subId = helperConfig.getConfig().subscriptionId;
fundSubscription(subId);
}
function fundSubscription(uint256 subId) public {
uint amount = 0.01 ether;
vm.startBroadcast();
MockContract(contractAddress).topUpSubscription(amount);
vm.stopBroadcast();
}
function run() public {
fundSubscriptionWithConfig();
}
}
contract AddConsumer is Script{
function addConsumerWithConfig() public {
HelperConfig helperConfig = new HelperConfig();
addConsumer();
}
function addConsumer() public {
vm.startBroadcast();
MockContract(contractAddress).addConsumers(address(0));
vm.stopBroadcast();
}
function run() public {
addConsumerWithConfig();
}
}
- This is the basic structure of writing HelperConfig and Interaction file.
By default, scripts are executed by calling the function named run, our entrypoint.
- This is the
pattern and best practice
we should followed!!!
Deploy.s.sol
import {Contract} from "../src/Contract.sol";
import {HelperConfig} from "./HelperConfig.s.sol";
import {FundSubscription, AddConsumer} from "./Interaction.s.sol";
contract MyScript is Script {
function setUp() public returns (Contract, HelperConfig){
// CREATED NEW HELPERNETWORK CONFIG INSTANCE
HelperConfig helperConfig = new HelperConfig();
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
// If for some valid condition we need to call the interaction.s.sol
if(condition){
// funding subscription
FundSubscription fundSubscription = new FundSubscription();
// add consumer after deployment
AddConsumer addConsumer = new AddConsumer();
}
vm.startBroadcast();
// pass all the constructor params here...
Contract token = new Contract(
config.priceFedd,
config.DataFeed,
);
vm.stopBroadcast();
return {token,helperConfig};
}
// BY DEFAULT forge script EXECUTES THE 'run' FUNCTION DURING DEPLOYMENT
function run() external returns(Contract,HelperConfig) {
return setUp();
}
}
change the .env and foundry.toml file
.env
# SEPOLIA TESTNET
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/{INFURA_KEY}
ETHERSCAN_API_KEY=
PRIVATE_KEY=
# ANVIL LOCALLY
LOCALLY_RPC_URL=http://localhost:8545
ANVIL_PRIVATE_KEY=
foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
'@chainlink/contracts@1.2.0/=lib/chainlink-brownie-contracts/contracts',
'forge-std/=lib/forge-std/src/',
'@solmate/=lib/solmate/src'
]
solc = "0.8.26"
via_ir = true
fs_permissions = [
{ access = "read", path = "./broadcast" },
{ access = "read", path = "./reports" },
]
[fuzz]
runs=256
- After deploying sc we can interact (send/call) the functions using cast
cast send <address> "setName(string)" "anurag" --rpc-url <rpc_url> --private-key <private_key>
cast call <address> "getName()"
cast to-base 0x7717 dec
// to use vanilla-foundry
foundryup
// to use L2/ROLLUPS
foundry-zksync
- For L2 and rollups you can refer there docs for more clearance
- --zksync refers that we are running on L2/rollups blockchain
-
The tests in Foundry are written in Solidity.
-
If the test function reverts, the test fails, otherwise it passes.
-
We will use VM Cheatcodes.
-
contract name starting with test are considered as a good practice in foundry
-
Forge Standard Library -> forge-std
-
UNIT TESTING - TESTING A SPECIFIC PARTs OF OUR CODE.
-
INTEGRATION TEST - TESTING THE INTERACTIONS PART OF OUR SMART CONTRACT
-
FORKED TEST - TESTING OUR CODE ON A SIMULATED REAL ENVIRONMENT(Sepolia or Rollups)
-
STAGING TEST - TESTING OUR CODE IN TESTNET/MAINNET. EX:- SEPOLIA, ANVIL LOCAL TESTING
-
FUZZ TESTING - identify vulnerabilities in a smart contract by systematically inputting random data values
- Stateful fuzz
- stateless fuzz
- formal verification
-
Forge supports testing in a forked environment
-
To run all tests in a forked environment, such as a forked Ethereum mainnet, pass an RPC URL via the --fork-url flag
-
Sometimes we need to run test from scratch. Before running test again remove the cache directory/forge clean
// TO LOAD THE .env CONTENT
source .env
echo $RPC_URL
// TESTING SC
forge test -vvv
forge test --fork-url $RPC_URL -vvvv
// TO RUN THE SINGLE TEST
forge test --mt testFunctionName
forge test --mt testBalance -vvv --fork-url $RPC_URL
// DEBUGGING SC
forge debug --debug src/Web3.sol:TestContract --sig "function(argu)" "arguValue"
// Verifiying smart contract on etherscan
forge test --fork-url <your_rpc_url> --etherscan-api-key <your_etherscan_api_key>
- Vm.sol: Up-to-date cheatcodes interface
- console.sol and console2.sol: Hardhat-style logging functionality
- Script.sol: Basic utilities for Solidity scripting
- Test.sol: A superset of DSTest containing standard libraries, a cheatcodes instance (vm), and Hardhat console
-
vm.prank(address(0))
- simulate a TNX to be sent from given specific address.
-
vm.deal(address(this), 1 ether)
- Used to give the test contract Ether to work with.
-
vm.expectRevert()
- Agar mera call/send function revert ho gaya, Toh mera test pass ho jayega.
- Else, test fail ho jayega.
-
vm.expectRevert(Contract.CustomError.selector)
- import the error from contract with 'selector'
-
vm.expectRevert(abi.enocodeSelector(Contract.CustomError.selector, params1, params2))
-
test_FunctionName
- Functions prefixed with 'test' are run as a test case by forge.
-
For, testFail
- A good practice is to use the pattern test_Revert[If|When]_Condition in combination with the expectRevert cheatcode
function test_RevertCannotSubtract43() public { vm.expectRevert(stdError.arithmeticError); testNumber -= 43; }
-
Test functions must have either external or public visibility.
-
type aliases(enum, struct, array,errors,events) can be call using main contract(Lottery) only.
function test_GetRaffleState() public view { assert(lottery.getLotteryStatus() == Lottery.LotteryStatus.Open); }
-
functions(call/send) can be called by our instance(lottery)
solidity function test_CheckEntranceFee() public view { assertEq(lottery.getEntryFeeAmount(), 0.01 ether); }
-
To Transfer some value during calling or Transact eth to SC
```solidity
function test_LotteryCheckIfUserIsAdded() external {
vm.prank(USER);
// by this method we pass some eth to our user.
lottery.enterLottery{value:_entranceFee}();
}
```
-
vm.expectEmit()
:- a specific log is emitted during the next call.
function test_LotteryEntranceFeeEvents() external{ vm.prank(USER); // for indexed params we will set it true vm.expectEmit(true, false, false,false , address(lottery)); emit EnteredUser(USER); lottery.enterLottery{value:_entranceFee}(); }
-
vm.warp() || vm.roll()
- Sets block.timestamp.
- Sets block.timestamp.
function test_UserNotAllowedToEnterLotteryWhenClosed() external { vm.prank(USER); lottery.enterLottery{value:_entranceFee}(); vm.warp(block.timestamp + _interval + 1); vm.roll(block.timestamp + 1); }
-
vm.recordLogs() || vm.getRecordedLogs()
- Tells the VM to start recording all the emitted events.
- To access them, use
getRecordedLogs
function test_GetEventsLogs() public { vm.recordLogs(); lottery.performUpkeep(""); Vm.Log[] memory logs = vm.getRecordedLogs(); bytes32 value = logs[1].topics[1]; assert(uint256(value) > 0); }
-
During testing with foundry, keep some point for best practices:
- Never make a variable public which contain imp. keys.
- Write
getterFunctions
- Only main contract can call
errors,events,structs,enums,types aliases
- Contract instance can call/send
getter n write functions
- continue...
- For, advance testing we will use
HelperConfig, MainContract and Deploy
file. - Follow,
Best practices and vm cheatcodes above for advance and better testing
.
Contract.t.sol
import {Contract} from "src/Contract.sol";
import {ContractScript} from "script/Deploy.s.sol";
import {HelperConfig,CodeConstants} from "script/HelperConfig.s.sol";
contract ContractTest is Test {
Contract contracts;
HelperConfig helperConfig;
// all constructor params and used variables
uint params1;
uint params2;
uint params3;
// events : Copy all events from contract to be used
/**
* here we will use our deploy script contract instance
* our deploy script setUp() returns 'Main contract' and 'HelperConfig contract'
* provide some eth to user for testing
*/
function setUp() public {
ContractScript contractScript = new ContractScript();
(contracts,helperConfig) = contractScript.setUp();
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
_param1 = config.param1;
_param2 = config.param2;
_param3 = config.param3;
// provide some eth to user for testing
vm.deal(address(0),1e18);
}
function test_GetContractStatus() public {
assert(contracts.getStatus() == Open);
}
function test_SomeChecks() external {
assert(contracts.getSomeVar() == 1 ether);
}
}
- Before running the forge remapping command we need to store the path in toml
- Forge can remap dependencies to make them easier to import. Forge will automatically try to deduce some remappings for you:
remapping = ['@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts']
- @chainlink/contracts now is equal to the actual path of contract
forge remappings
- Displays which parts of your code are covered by tests.
// View summarized coverage:
forge coverage
// Create lcov file with coverage data:
forge coverage --report lcov
// This will create a .txt file that will give us the parts of our contracts cover:
forge coverage --report debug > coverage.txt
A repo to get the most recent deployment from a given environment in foundry. This way, you can do scripting off previous deployments in solidity.
It will look through your broadcast
folder at your most recent deployment.
- Get the most recent deployment of a contract in foundry
- Checking if you're on a zkSync based chain
- Update forge-std to use newer FS cheatcodes
forge install Cyfrin/foundry-devops --no-commit
forge install foundry-rs/forge-std@v1.8.2 --no-commit
1. Update your foundry.toml
to have read permissions on the broadcast
folder.
fs_permissions = [
{ access = "read", path = "./broadcast" },
{ access = "read", path = "./reports" },
]
- Import the package, and call
DevOpsTools.get_most_recent_deployment("MyContract", chainid);
ie:
import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol";
import {MyContract} from "my-contract/MyContract.sol";
.
.
.
function interactWithPreviouslyDeployedContracts() public {
address contractAddress = DevOpsTools.get_most_recent_deployment("MyContract", block.chainid);
MyContract myContract = MyContract(contractAddress);
myContract.doSomething();
}
- foundry-zksync
- You'll know you did it right if you can run
foundryup-zksync --help
and you see a response like:
- You'll know you did it right if you can run
The installer for Foundry-zksync.
Update or revert to a specific Foundry-zksync version with ease.
.
.
.
In your contract, you can import and inherit the abstract contract ZkSyncChainChecker
to check if you are on a zkSync based chain. And add the skipZkSync
modifier to any function you want to skip if you are on a zkSync based chain.
It will check both the precompiles or the chainid
to determine if you are on a zkSync based chain.
import {ZkSyncChainChecker} from "lib/foundry-devops/src/ZkSyncChainChecker.sol";
contract MyContract is ZkSyncChainChecker {
function doStuff() skipZkSync {
skipZkSync
: Skips the function if you are on a zkSync based chain.onlyZkSync
: Only allows the function if you are on a zkSync based chain.
isZkSyncChain()
: Returns true if you are on a zkSync based chain.isOnZkSyncPrecompiles()
: Returns true if you are on a zkSync based chain using the precompiles.isOnZkSyncChainId()
: Returns true if you are on a zkSync based chain using the chainid.
In your contract, you can import and inherit the abstract contract FoundryZkSyncChecker
to check if you are on the foundry-zksync
fork of foundry
.
!Important: Functions and modifiers in
FoundryZkSyncChecker
are only available if you runfoundry-zksync
with the--zksync
flag.
import {FoundryZkSyncChecker} from "lib/foundry-devops/src/FoundryZkSyncChecker.sol";
contract MyContract is FoundryZkSyncChecker {
function doStuff() onlyFoundryZkSync {
You must also add ffi = true
to your foundry.toml
to use this feature.
onlyFoundryZkSync
: Only allows the function if you are onfoundry-zksync
onlyVanillaFoundry
: Only allows the function if you are onfoundry
is_foundry_zksync
: Returns true if you are onfoundry-zksync