updraft
Other cast commands
Convert hexadecimal
cast --to-base 0x714e1 dec
send
cast send 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6 "store(uint256)" 1337 --rpc-url $RPC_URL --account minner-key
call
cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "retrieve()"
cast wallet
Official docs — there are also a few commands related to mnemonics, as well as commands for signing.
The encrypted data lives in the .foundry/keystores folder under your home directory.
Create an address
cast wallet new or n
cast wallet n
Import an account
cast wallet import or i
cast wallet import xxxx-name --private-key xxxxxxxx-key pass the private key in plaintext directly
cast wallet i xxxx-name --interactive enter the private key interactively
Best practice:
Open a new terminal
cast wallet i xxxx-name --interactive enter the private key interactively
then
history -c
close the terminal
Remove
To remove an account you must pass --name
cast wallet remove or rm
cast wallet remove --name anvil-02
List
cast wallet list or ls list the saved account aliases
cast wallet ls
Look up the public key by alias
cast wallet address or a or addr
cast wallet address --account anvil-01
Look up the private key by alias
cast wallet decrypt-keystore or dk
cast wallet decrypt-keystore anvil-01
env
In the .env file there must be no spaces around the =.
source .env
echo $PRIVATE_KEY
.env template
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/xxxxx
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/xxxxxx
ARBITRUM_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/xxxxxxx
ANVIL_RPC_URL=http://127.0.0.1:8545
ANVIL_PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxx
makeFile
foundry.toml file
forge test
forge test --mt testPriceFeedVersionIsAccurate
Understanding the cheatcodes
assertEq (fundMe.i_owner ( ), address ( this )); // assert equality
hoax = deal + prank add ETH directly + switch msg.sender
address alice = makeAddr("alice"); create the user alice
vm.deal(alice, BALANCE) give alice some funds
vm.prank(alice) the next line is executed as alice
vm.startPrank(alice)
...all lines in between are executed as alice
vm.stopPrank()
vm.startBroadcast(); // marks the code in between as transactions to be sent; a transaction is any action that modifies on-chain state
HelperConfig helperConfig = new HelperConfig();
FundMe fundMe = new FundMe(helperConfig.activeNetworkConfig());
vm.stopBroadcast();
vm.warp(block.timestamp + interval + 1); // change the timestamp
vm.roll(block.number + 1); // increment the block number by one
vm.txGasPrice(GAS_PRICE); a Foundry cheatcode used to change tx.gasprice
vm.envBytes32() read a bytes32 value from an environment variable
vm.envUint() read a uint256 value from an environment variable
Testing whether an event is emitted
First re-declare the event in the test file, then:
function testEmitsEventOnEntrance() public {
vm.prank(alice);
// the event we expect to be emitted
vm.expectEmit(true, false, false, false, address(raffle));
// emit it ourselves once in the test file
emit EnteredRaffle(alice);
// call the actual code
raffle.enterRaffle{value: 0.5 ether}();
}
Testing event parameters
vm.recordLogs(); // start recording
raffle.performUpkeep(""); // trigger
Vm.Log[] memory entries = vm.getRecordedLogs(); // get the recorded logs
bytes32 requestId = entries[1].topics[1]; // extract the target data
Testing custom errors
You need the selector of the custom error, ABI-encode it together with the revert arguments to define exactly the expected revert, and then trigger the call.
vm.expectRevert(); // just expects a revert; passes as long as something reverts, regardless of the error name or type
vm.expectRevert(
abi.encodeWithSelector(
Raffle.Raffle_UpkeepNotNeeded.selector,
0,
0,
raffleStatus
)
);
vm.prank(alice);
raffle.performUpkeep("");
Fuzz testing
If a Foundry test method takes parameters, it becomes a fuzz test, and Foundry automatically supplies random values for those parameters.
Fuzz tests use bound to constrain the range of a parameter.
uint256 additionalEntrants = bound(additionalEntrantsNum, 1, 10000);
Test coverage
the --report xxx flag takes an option; debug is also viewable, while summary is essentially the same as plain forge coverage
forge coverage --fork-url $SEPOLIA_RPC_URL
forge coverage --report debug > coverage.txt
What the number of -v's means
Verbosity levels:
- 2: Print logs for all tests
- 3: Print execution traces for failing tests
- 4: Print execution traces for all tests, and setup traces for failing tests
- 5: Print execution and setup traces for all tests
Types of tests
Unit tests: focus on isolating and testing individual functions or features of a smart contract. Integration tests: verify how a smart contract interacts with other contracts or external systems. Forking tests: a fork is a copy of blockchain state at a specific point in time. This copy (called a fork) is then used to run tests in a simulated environment. Staging tests: run tests against an already-deployed smart contract in a staging environment before deploying to mainnet.
Installing dependencies
The GitHub repo is https://github.com/smartcontractkit/chainlink-brownie-contracts Install command: forge install smartcontractkit/chainlink-brownie-contracts@0.6.1
Install address for Chainlink VRF 2.5: forge install /smartcontractkit/chainlink-brownie-contracts Project link: https://github.com/smartcontractkit/chainlink-brownie-contracts
About gas
Check gas prices: https://etherscan.io/gastracker Conversion tool: https://www.alchemy.com/gwei-calculator Prices: https://coinmarketcap.com/
forge snapshot --mt testOwnerIsMsgSender
Gas consumption
gasleft() a Solidity function that returns the remaining gas
tx.gasprice; a Solidity property holding the gas price of the current transaction
vm.txGasPrice(GAS_PRICE); a Foundry cheatcode used to change tx.gasprice
Gas consumption issue: declaring an array and then reassigning it to a fresh array actually consumes a lot of gas, growing with the number of elements. Instead, use a mapping combined with a counter: store the addresses in the mapping and count them with the counter; when you need to clear it, just set the counter back to 0 and start overwriting from the beginning.
address payable[] s_players
s_players = new address payable[](0)
mapping(uint256 => address payable) s_players;
uint256 s_players = 0;
Measuring the gas consumed by a single call
function testWithdrawFromASingleFunder() public funded {
uint256 startingFundMeBalance = address(fundMe).balance;
uint256 startingOwnerBalance = owner.balance;
vm.txGasPrice(GAS_PRICE);
uint256 gasStart = gasleft();
vm.startPrank(owner);
fundMe.withdraw();
vm.stopPrank();
uint256 gasEnd = gasleft();
uint256 gasUsed = (gasStart - gasEnd) * tx.gasprice;
console2.log("Withdraw consumed: %d gas", gasUsed);
console2.log("tx.gasprice", tx.gasprice);
uint256 endingFundMeBalance = address(fundMe).balance;
uint256 endingOwnerBalance = owner.balance;
assertEq(endingFundMeBalance, 0);
assertEq(startingOwnerBalance + startingFundMeBalance, endingOwnerBalance);
}
Analyzing gas through storage
Lesson 21 of FundMe explains how to save gas from the perspective of the storage layout.
vm.load(); load storage data forge inspect FundMe storageLayout inspect the storage layout cast storage 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 2 inspect storage by contract address
Miscellaneous
forge init initialize a project forge init --force
forge fmt format the code; switch from auto-save to a deliberate save (e.g. cursor-move triggered), without auto-save, and format manually with forge fmt
anvil start a local virtual environment Deployment: it currently seems you need --broadcast, which means to broadcast and perform the actual deployment; without it, the deployment is only simulated
$ forge create SimpleStorage --private-key ac0974bec39a17e36ba4a6b4d238fcbed5efcae784d7bf4f2ff80 --rpc-url http://127.0.0.1:8545 --broadcast
Without a private key, you need to know the unlocked and from addresses
$ forge create SimpleStorage --unlocked --from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:8545 --broadcast
Deploy via a script forge script script/SimpleStorage.s.sol --private-key ac0974bec39a17e3478cbed5efcae784d7bf4f2ff80 --rpc-url http://127.0.0.1:8545 --broadcast
forge create SimpleStorage --interactive enter the private key interactively Without --rpc-url, the Anvil network is used by default
Foundry best practices: https://www.getfoundry.sh/best-practices#scripts forge-std, the Forge Standard Library, is designed to simplify and enhance scripting and testing within the Foundry development framework
Regarding scripts: running a script populates the broadcast folder, which holds the execution records. Every script must inherit Script and implement a run method with external visibility. Scripts all end with .s.sol
forge script script/SimpleStorage.s.sol --rpc-url $RPC_KEY --broadcast --account minner-key --sender 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Deploy forge script script/SimpleStorage.s.sol --rpc-url $RPC_URL_SEPOLIA --broadcast --account sepolia-01 --with-gas-price 2gwei -vvvv
forge script script/DeployFundMe.s.sol:DeployFundMe --rpc-url (PRIVATE_KEY) --broadcast --verify --etherscan-api-key $(ETHERSCAN_API_KEY) -vvvv
When deploying a contract, if you call new xx() directly inside a test contract, then msg.sender is the test contract. If you use vm.startBroadcast(), msg.sender becomes the address of our default external account. This address has nothing to do with the Anvil chain; it is a built-in account used during testing.
uint160 can be cast directly to the address type, but uint256 cannot.
chisel is an interactive environment.
Deploy
$ forge script DeployFundMe --rpc-url $ANVIL_RPC_URL --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast
Monad contract verification
forge verify-contract \
--rpc-url https://testnet-rpc.monad.xyz \
--verifier sourcify \
--verifier-url 'https://sourcify-api-monad.blockvision.org/' \
0xdE3651Ada44138305Af580E2627d343D6C66d0Eb \
src/FundMe.sol:FundMe
forge install Cyfrin/foundry-devops install the package
Code style
NATSPEC comments
Official docs: https://docs.soliditylang.org/zh-cn/v0.8.24/natspec-format.html#natspec
@title
A title that should describe the contract/interface
contract, library, interface, struct, enum, enum values
@author
The name of the author
contract, library, interface, struct, enum, enum values
@notice
Explain to an end user what this does
contract, library, interface, function, public state variable, event, struct, enum, enum values error
@dev
Explain to a developer any extra details
contract, library, interface, function, state variable, event, struct, enum, enum values, error
@param
Documents a parameter just like in Doxygen (must be followed by parameter name)
function, event, enum values, error
@return
Documents the return variables of a contract’s function
function, enum, enum values, public state variable
@inheritdoc
Copies all missing tags from the base function (must be followed by the contract name)
function, enum, enum values, public state variable
@custom:...
Custom tag, semantics is application-defined
everywhere
Order of code layout
https://docs.soliditylang.org/en/latest/style-guide.html#order-of-layout
// Layout of the contract file:
// version
// imports
// errors
// interfaces, libraries, contract
// Inside Contract:
// Type declarations
// State variables
// Events
// Modifiers
// Functions
// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// view & pure functions
Variable naming conventions
Prefix state variables with s_ Prefix immutable variables with i_ Prefix custom errors with the contract name: error Raffle_NotEnoughEthSend();
Events are not used arbitrarily; emit them when state variables change.
CEI pattern: Checks first, then Effects, and finally Interactions
function coolFunction() public {
// Checks
checkX();
checkY();
// Effects
updateStateM();
// Interactions
sendA();
callB();
}
--fork-url fork the relevant state of a chain locally to run tests against --rpc-url specify which chain to act on; only valid when deploying (works for script, not for test)
--broadcast broadcast, i.e. send the transactions on-chain; without it, running a script only simulates execution
A library that provides ERC20.
forge install transmissions11/solmate --no-commit
@solmate/=lib/solmate/src
Make git ignore a submodule git config submodule.lib/chainlink-brownie-contracts.ignore all
forge install Cyfrin/foundry-devops --no-commit
-f force-uninstall a package forge remove smartcontractkit/chainlink-brownie-contracts -f
After cloning a fresh repo from GitHub, use this command to initialize and install the dependencies git submodule update --init --recursive
After installing a library and then repeatedly uninstalling and reinstalling it back and forth
git submodule deinit -f -- lib/openzeppelin-contracts-upgradeable
git rm -f lib/openzeppelin-contracts-upgradeable
rm -rf .git/modules/lib/openzeppelin-contracts-upgradeable
rm -rf lib/openzeppelin-contracts-upgradeable
forge install OpenZeppelin/openzeppelin-contracts-upgradeable@release-v4.9 # pin the release-v4.9 branch
forge build
MLaunch The onlyOwner / initializer question
function initialize(
PositionManager _positionManager,
address _memecoinTreasuryImplementation
) external onlyOwner initializer {
positionManager = _positionManager;
memecoinTreasuryImplementation = _memecoinTreasuryImplementation;
}
constructor(address _memecoinImplementation, string memory _baseURI) {
s_memecoinImplementation = _memecoinImplementation;
s_baseURI = _baseURI; ??????????
_initializeOwner(msg.sender); ?????????????
}
The three abstract methods of ERC-721 — what does baseURI mean?