- deploy contracts from other contract using
new
import "./myOtherContract.sol";
- name the contract being imported to be more ogranized
import { Contract as MyContract } from "./myOtherContract.sol";
- interact with other contracts as long as we have the ABI and contract's address
- Can create a "child" contract that inherits the features of another contract.
- Import the parent contract and use the
is
keyword - to override a function of the parent contract you use the
override
keyword in the child contract and need to use thevirtual
keyword on the function in the parent contract - see SimpleStorage.sol and AddFiveStorage.sol store function for example
-
When using
forge script
if there is no RPC URL it will spin up a temporary Anvil to deploy the contract -
cast --to-base <hex> dec
will convert hex into decimalcast --help
shows other conversions and tools you can use with cast
- .env file make sure is listed in
.gitignore
- after make to
source .env
to make sure it grabs from .env file - can useecho $<.env_VARIABLE>
to make sure it's getting it- can add .env variable to use with terminal command with
$<.env_VARIABLE>
- can add .env variable to use with terminal command with
- after make to
- For real money you'll use
--interactive
or a keystoree file with a password - With
cast
you can docast wallet import defaultKey --interactive
to create your keystore with a password cast wallet list
shows full lists of keystore wallets--password-file
could be used too to make it faster instead of having to put in your password that would come from something like a.password
file- make sure to add
.password
file to.gitignore
- Can also create a
ETH_PASSWORD
variable in a .env and use--password $ETH_PASSWORD
instead of putting in your password everytime
- View history with
history
- clear history with
history -c
- clear terminal with
clear
- delete a word [ctrl] + W
- clear to the beginning [ctrl] + U
rm .bash_history
to clear command line history
-
You can see your "runs" in the
broadcast
folder under the contract name and the chain ID- give you details about the txn
run-latest.json
will always be the latest run
-
forge fmt
formats the smart contracts -
foundryup
to spin up "vanilla" Foundryfoundryup-zksync
to spin up ZKsync Foundry
- use
forge create
overforge script
for now npx zksync-cli dev config
starts CLI with setting up era test node with a block explorer or a portal- make sure to use the space bar to select the portal or block explorer with the era test node
--legacy
flag sends transaction as0x0
(pre-EIP1559 and before an intro of typed-txns)
forge --init
will create project and give all the folders for Foundryforge
: compiling and interacting with contractscast
: interacting with contracts that have already been deployedcast send
to send txnscast call
to read from contracts
anvil
: deploy a local blockchain- When you send a txn, you're making an HTTP Post request to the RPC URL
forge install <name_of_org/name_of_repo>
- you can also put the full Github repo URL in as well and it will work
- create a remapping to tell Foundry where to find the import in the
lib
folderfoundry.toml
addremappings = ["<file you want Foundry to remap>=lib<file you want to map to in lib>"]
forge build
will compile your project- When naming errors, start with contract name and then
__
(double underscores) so you can tell what contract the error came from
-
importing from
forge-std/Test.sol
gives you theassert
function -
setUp
is always ran beforetestDemo
which is why the test below passescontract FundMeTest is Test { uint256 number = 1; function setUp() external { number = 2; } // setUp will always run first even if there is code before it function testDemo() public { assertEq(number, 2); } }
-
Console Logging
console.log()
- can get things printed out from our tests
- make sure to import from
forge-std/Test.sol
import {Test, console} from "forge-std/Test.sol";
-
forge test -vv
-v
is the level of logs shown from-v
to-vvvvv
- more "v" the more detailed the logs
-vv
will showconsole.log()
s
-
import {FundMe} from "../src/FundMe.sol";
..
stands for going down a directory
-
when testing, we can test functions or variables that are public functions in the OG smart contract
- make test names very verbose so you know exactly what is being tested
function testMinimumDollarIsFive() public {}
instead offunction testDemo() public {}
- make test names very verbose so you know exactly what is being tested
-
the line
FundMe fundMe;
is necessary because it declares a state variable that allows you to access theFundMe
contract instance throughout the contract's functions.contract FundMeTest is Test { FundMe fundMe; // declares state variable of type FundMe so that it's accessible by all functions within the FundMeTest contract function setUp() external { fundMe = new FundMe(); // "fundMe" variable of type "FundMe" is going to be a new FundMe contract } function testMinimumDollarIsFive() public { assertEq(fundMe.MINIMUM_USD(), 5e18); // to call this we need access to "fundMe" } }
-
State Variable:
FundMe fundMe;
This declares a state variable namedfundMe
of typeFundMe
. It tells the Solidity compiler that there will be a variable calledfundMe
in this contract that will hold an instance of aFundMe
contract. This variable is accessible by all functions within theFundMeTest
contract. State variables are stored on the blockchain and persist between function calls.- The line
FundMe fundMe;
is a declaration of a state variable, but it does not initialize or create an instance of theFundMe
contract. It merely sets up a placeholder (or a reference) for aFundMe
contract instance. Here’s why you still needfundMe = new FundMe();
:- Without the
new FundMe();
line, thefundMe
variable does not refer to any actual contract. Trying to interact withfundMe
without initializing it would lead to errors because there is no contract instance to interact with. - The
new FundMe();
line effectively deploys theFundMe
contract to the blockchain within your test environment and assigns the deployed contract’s address to thefundMe
state variable. This deployment is necessary to test how yourFundMeTest
contract interacts with a liveFundMe
contract.
- Without the
-
Local Variable in
setUp()
:- In the
setUp()
function, the lineFundMe fundMe = new FundMe();
declares a local variablefundMe
that is scoped only to thesetUp()
function. This variable is different from the state variablefundMe
declared at the top of the contract. - Since this
fundMe
variable is local tosetUp()
, it is destroyed aftersetUp()
completes, meaning you cannot access it in other functions liketestMinimumDollarIsFive()
.
- In the
-
State vs. Local Variables:
- State Variable (
FundMe fundMe;
): This is accessible throughout the contract and retains its value across different function calls. When you create the contract instance insetUp()
and assign it to this state variable, you can access it in any test function. - Local Variable (
FundMe fundMe = new FundMe();
): This is only accessible within thesetUp()
function. OncesetUp()
completes, this variable is no longer accessible, which is whytestMinimumDollarIsFive()
cannot access thefundMe
instance created insetUp()
unless it was assigned to the state variable.
- State Variable (
-
fundMe.MINIMUM_USD()
- You can access
fundMe.MINIMUM_USD()
becauseMINIMUM_USD
is a public state variable or constant in theFundMe
contract. Solidity automatically generates a getter function for any public variable, allowing you to access its value through this function.
- You can access
-
address(this)
gets the Ethereum address of that contract instance -
msg.sender
is the address that called the current function
can use
console.log()
for both of these to find out which addresses they are
vm.startBroadcast();
...
v,.stopBroadcast();
-
tells foundry to start deploy and when to finis the deployment
-
forge clean
will clear the cache of your Foundry files which deletes Build Artificats and removes Cache Files
forge test --mt <test_function>
allows you to test one specific test in your test file- if you don't state which network it will test on an anvil one and close it after
forge coverage --<fork-url or rpc_url> <URL>
shows how many lines of code are actually tested- want this to be in higher % of tested lines (maximize your tests)
- testing a specific part of your code
- example (also includes integration because we're checking the version from another contract):
function testPriceFeedVersionIsAccurate() public { uint256 version = fundMe.getVersion(); console.log(version); assertEq(version, 4); }
- example (also includes integration because we're checking the version from another contract):
- testing how your code works with other parts of your code
- testing your code on a simulated real environment
forge test --mt testPriceFeedVersionIsAccurate -vvv --fork-url $SEPOLIA_RPC_URL
- spins up an anvil copy of the URL (Sepolia in the above example) and simulate all the txns without having to actually deploy
- this still makes calls to your RPC URL which could cause increased cost of RPC URL if using a third party like Alchemy
- Why?
- you're still making calls to the RPC URL to retrieve the latest state data for the simulations
- Why?
- testing your code in a real environment that is not prod
- make contracts open to be able to be deployed on other networks to interact with contracts on those chains
- not hardcoding addresses or networks
- refactor: change up the code architecture but not the functionality. Keeps code maintainable moving forward
- variables before the constructor are state variables and remain persistent throughout the lifetime of the contract
- variables inside the constructor are executed only once at the time of contract deployment
- can set
immutable
variables here since it consumes less gas than setting as state variable
-
Example
-
instead of hard coded address for
AggregatorV3Interface
we can add a variable to the constructor that will setup an instance of theAggregator V3Interface
stored ins_priceFeed
based on the address passed at deployment (the address for the contract on the chain)contract FundMe { using PriceConverter for uint256; mapping(address => uint256) public addressToAmountFunded; address[] public funders; // Could we make this constant? /* hint: no! We should make it immutable! */ address public /* immutable */ i_owner; uint256 public constant MINIMUM_USD = 5 * 10 ** 18; AggregatorV3Interface private s_priceFeed; // <-- constructor(address priceFeed) { // <-- i_owner = msg.sender; s_priceFeed = AggregatorV3Interface(priceFeed); // <-- }
- Checkout
PriceConverter.sol
for changes to make more modular too
- Checkout
-
-
How to make changes to deploy script without having to make changes to test too
- Import the contract from deploy script to the test so that it will deploy the same was as in our script
- example
FundMeTest.t.sol
- See more notes in
DeployFundMe.s.sol
- example
- Import the contract from deploy script to the test so that it will deploy the same was as in our script
-
deploy a mock contract on anvil so you don't have to spend resources on infra provider (Alchemy)
- see
HelperConfig.s.sol
for examples of how to deploy a mock on anvil chain and keep track of contract addresses when connected to other chains- work with
structs
,memory
, andNetworkConfig
- have to import
HelperConfig
inDeployFundMe.s.sol
- create variable for the price feed based on the
activeNetworkConfig
and deploy theFundMe
contract passing the address for the priceFeed on the matching network
- create variable for the price feed based on the
- Anvil setup will be different since the mentioned contracts do not exist on Anvil
- have to deploy them ourselves on Anvil
- Deploy the mocks
- Return the mock addresses
- can no longer be a
pure
function since we have to usevm.startBroadcast();
to deploy the mock contracts- also have to change
contract HelperConfig {
tocontract HelperConfig is Script {
so that it can have access to thevm
keyword
- also have to change
- create a
mocks
folder intest
so that you can separate the mock contracts from the real ones
- work with
- see
-
in the AnvilEthConfig add an if statement so that it doesn't redeploy a mock priceFeed contract if there is already and address for
priceFeed
function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) { // price feed address if (activeNetworkConfig.priceFeed != address(0)) { // if there was already a priceFeed address for the anvil config, don't run the rest of the code below. If priceFeed address is not 0 return activeNetworkConfig; } vm.startBroadcast(); ...
-
numbers in code that are not defined directly in the code but in another contract
- example
8
and2000e8
refer to the_decimals
and_initialAnswer
inMockV3Aggregator.sol
MockV3Aggregator mockPriceFeed = new MockV3Aggregator(8, 2000e8);
-
can turn these into constants at the top of the contract
uint8 public constant DECIMALS = 8; int256 public constant INITIAL_PRICE = 2000e8;
- and update:
vm.startBroadcast(); MockV3Aggregator mockPriceFeed = new MockV3Aggregator( DECIMALS, INITIAL_PRICE ); vm.stopBroadcast();
- example
-
https://book.getfoundry.sh/forge/cheatcodes
- Can also check out https://book.getfoundry.sh/cheatcodes/ for looking at different cheatcode types
function testFundFailsWithoutEnoughETH() public { vm.expectRevert(); // the next line should revert // ignores `vm.` for example when setting a `prank` address fundMe.fund(); // fails because nothing is passed through the fund function which requires more than or equal to `MINIMUM_USD` }
In Solidity, when you want to send Ether along with a function call, you use the {value: X} syntax inside the curly braces. This is known as a “function call option” and is used to specify additional parameters for the function call, such as the amount of Ether to send (value), the gas limit (gas), or the sender’s address (from).
contract FundMe {
using PriceConverter for uint256;
mapping(address => uint256) public s_addressToAmountFunded; // Keeps track of the amount of funds contributed by each address.
address[] public s_funders; // stores the list of all addresses that have contributed funds to the contract.
...
-
For the storage variables, start with
s_
to help make distinctive- default all the storage variables to
private
to make more gas efficient-
if need public can signal that for specific ones
- create
view
/pure
functions or "getters" to be able to ask the private storage variables
function getAddressToAmountFunded( address fundingAddress ) external view returns (uint256) { return s_addressToAmountFunded[fundingAddress]; }
- create
-
- default all the storage variables to
-
prank
sets themsg.sender
to the specified address for the next call- use to know who is sending the call instead of not knowing if it's
msg.sender
orfundMe
(address(this)
)
- use to know who is sending the call instead of not knowing if it's
Use
prank
after settingmakeAddr
seeFundMeTest.t.sol
-
makeAddr
makes a new address based on a name passed-
added to top of
FundMeTest.t.sol
so you can use it throughout the testsaddress USER = makeAddr("user"); ... function testFundUpdatesFundedDataStructure() public { vm.prank(USER); ...
-
-
vm.startPrank
/vm.stopPrank
- same as
vm.startBroadcast
where you run everything within the vm - example:
vm.startPrank(fundMe.getOwner()); fundMe.withdraw(); vm.stopPrank();
- same as
-
deal
set the balance of an address- set a constant variable with an amount
uint256 constant STARTIN_BALANCE = 10 ether;
- in the
setUp
function add deal withvm
vm.deal(USER, STARTING_BALANCE);
-
modifier
allows you to create a state that can be used in other tests so you don't have to continue to enter the same cheatcodesmodifier
needs a_;
at the end to signal where the code is executed after it. If the _ is omitted, the function body never gets executed, and only the modifier’s logic will run.- example:
modifier funded() { vm.prank(USER); fundMe.fund{value: SEND_VALUE}(); _; }
- add
modifier
to function in test:
function testOnlyOwnerCanWithdraw() public funded { vm.expectRevert(); vm.prank(USER); fundMe.withdraw(); }
Fun fact:
- (): Used to pass parameters defined in the function’s signature (e.g., uint256 amount).
- {}: Used to pass optional transaction parameters like Ether (value), gas (gas), or sender (from), which are outside the function signature.
txGasPrice
sets the tx.gasprice for the rest of hte txn
Way to think about setting up a test
- Arrange the test
- Do the action you want to test
- Assert the test
-
see
testWithdrawWithASingleFunder
function inFundMeTest.t.sol
for example of arrange, act, assert -
hoax
-
sets up a
prank
from an address that has some ether- puts
prank
anddeal
together
- puts
-
hoax(<ADDRESS>, SEND_VALUE);
-
when setting up addresses with
address(NUMBER)
you'll need to wrap it in auint160
instead of 256- example:
function testWithdrawFromMultipleFunders() public funded { uint256 numberOfFunders = 10; => uint160 numberOfFunders = 10; uint256 startingFunderIndex = 2; => uint160 startingFunderIndex = 2; for (uint160 i = startingFunderIndex; i < numberOfFunders; i++) {} }
-
-
assert
andassertEq
assertEq
provides more detailed error messages, including the expected and actual values, which can be extremely useful when troubleshooting.assertEq
is generally preferred in test cases because of the additional information it provides when the test fails, making it easier identify the issue.
assert
only checks a condition but doesn’t provide as much detail on failure beyond the fact that the condition was false.
Let's you write in solidity in the terminal and execute it line by line
chisel
-
How the contract stores things like the state variables (aka storage variables)
-
Global or state variables that stay permanent, they are stuck in storage
Think of storage as a giant array or list of variables that we create
-
each variable is stored in a "slot" and each slot is 32 bytes long, representing the bytes version of the object
Example:
contract FunWithStorage {
uint256 favoriteNumber;
constructor() {
favoriteNumber = 25;
}
}
The uint256 25 is stored in the first slot (slot 0) and represented in hex as 0x000...0019
contract FunWithStorage {
uint256 favoriteNumber;
bool someBool;
constructor() {
favoriteNumber = 25;
someBool = true;
}
}
For a "true" boolean, it would be 0x000...001 in hex
contract FunWithStorage {
uint256 favoriteNumber;
bool someBool;
uint256[] myArray;
constructor() {
favoriteNumber = 25;
someBool = true;
myArray.push(222);
}
}
For dynamic values like mappings and dynamic arrays, the elements are stored using a hashing function (see solidity dos for more details).
- For arrays, a sequential storage spot is taken up for the length of the array
- For mappings, a sequential storage spot is taken up, but left blank and solidity will know what to do with it
Constant and immutable variables do not take up slots in storage because they are part of the contracts bytecode
- variables within a function only last for the duration of the function and
do not persist
- they get added in their own memory structure that gets deleted after
forge <CONTRACT_NAME> storageLayout
will show the storage layout of the
contract to see what variables are stored at what storage slots
"object" in bytecode is the contract in pure bytecode "opcodes" show the used opcodes in the contract. The actual things that our contract should do.
- Each opcode has a gas cost for it
- https://www.evm.codes/ for list of opcodes and their costs
ex.
SLOAD
is 100 gas. Anytime you read from storage it's 100 gas!MLOAD
is 3 gas so we can save on gas by using Memory
cheaperWithdraw |
withdraw |
---|---|
solidity<br>function cheaperWithdraw() public onlyOwner {<br> uint256 fundersLength = s_funders.length;<br> for (uint256 funderIndex = 0; funderIndex < fundersLength; funderIndex++) {<br> address funder = s_funders;<br> (bool callSuccess, ) = payable(msg.sender).call{value: address(this).balance}("");<br> require(callSuccess, "Call failed");<br>} |
solidity<br>function withdraw() public onlyOwner {<br> for (uint256 funderIndex = 0; funderIndex < s_funders.length; funderIndex++) {<br> address funder = s_funders;<br> (bool callSuccess, ) = payable(msg.sender).call{value: address(this).balance}("");<br> require(callSuccess, "Call failed");<br>} |
Line | cheaperWithdraw |
withdraw |
---|---|---|
1 | function cheaperWithdraw() public onlyOwner { |
function withdraw() public onlyOwner { |
2 | uint256 fundersLength = s_funders.length; |
(Line not present) |
3 | for (uint256 funderIndex = 0; funderIndex < fundersLength; funderIndex++) { |
for (uint256 funderIndex = 0; funderIndex < s_funders.length; funderIndex++) { |
4 | address funder = s_funders[funderIndex]; |
address funder = s_funders[funderIndex]; |
5 | s_addressToAmountFunded[funder] = 0; |
s_addressToAmountFunded[funder] = 0; |
6 | } |
} |
7 | s_funders = new address; |
s_funders = new address; |
8 | (bool callSuccess, ) = payable(msg.sender).call{value: address(this).balance}(""); |
(bool callSuccess, ) = payable(msg.sender).call{value: address(this).balance}(""); |
9 | require(callSuccess, "Call failed"); |
require(callSuccess, "Call failed"); |
10 | } |
} |
- Line 2 (
cheaperWithdraw
only):cheaperWithdraw
declares a local variablefundersLength
to store the length ofs_funders
.
- Line 3 (Loop Condition):
cheaperWithdraw
usesfundersLength
in the loop condition.withdraw
accessess_funders.length
directly in the loop condition.
- Optimization in
cheaperWithdraw
:- Storage Reads vs. Memory Reads:
- Accessing storage variables like
s_funders.length
is more gas-intensive than accessing memory variables. - By storing
s_funders.length
in a local memory variablefundersLength
, the function reduces the number of storage reads.
- Accessing storage variables like
- Gas Efficiency:
- This change makes
cheaperWithdraw
more gas-efficient, especially for loops with many iterations.
- This change makes
- Storage Reads vs. Memory Reads:
- Standard Approach in
withdraw
:- The
withdraw
function readss_funders.length
from storage during each loop iteration. - This approach is less gas-efficient due to multiple storage reads.
- The
Solidity Style Guide for code layout to be more readable.
- use
s_
for variables stored in storage - use
i_
for immutable variables - use all caps for
CONSTANTS
- explain what codes does and how to do it
-
Interactions.s.sol
have all the ways to interact with our contract -
install Foundry devops for tools to use with foundry such as a script that grabs the most recent deployment of a contract to test integrations
-
forge install Cyfrin/foundry-devops --no-commit
-
You can then import it in the
Interactions.s.sol
contract but need to setffi = true
in yourfoundry.toml
fileYou don't always want to use this as it let's foundry run commands directly on your machine
-
DevOpsTools.get_most_recent_deployment
can be used to get the most recent deployment address so you don't have to manually add the contract address you want to interact with every time- looks inside the
broadcast
folder in therun-latest.json
and grabs the most recently deployed contract in that file
- looks inside the
-
-
Split Integration tests and Unit tests into different folders to keep track
Interactions.s.sol
is for testing the main functions of my contractFundMeTest.t.sol
for testing different parts of the contract
- allow you to create shortcuts for commands we are going to commonly use
- add a
Makefile
in your project - allows you to automatically grab from variables from
.env
without having to runsource .env
- need to add () around your environment variables
ZksyncDevOps.t.sol
used to test things that will work on vanilla Foundry, but
not on ZKsync Foundry and vice-versa
FoundryZkSyncChecker
to run test on either foundry-zksync or vanilla foundry
ZkSyncChainChecker
to run tests only on ZKsync Era or on other EVM chains
Can learn more here.
In your .gitignore
file you can do <FILE_NAME>/
to prevent it from being
pushed publicly
git push origin main
pushes the commits to the repo on the main branch
- can do
git remote -v
to see what the origin is
Better understanding how wallets are interacting with the frontend and making sure you're signing what you're supposed to sign
- Can use Liver Server extension to "Go Live" and have VS Code or Cursor host the website
The Console is a live javascript shell which has a lot of info about the browser we're working with
-
Metamask injects into the javascript shell in the
window
object- specifically it injects
window.ethereum
into the browser and this is how the website sends transactions to our wallet
- specifically it injects
-
index.js
will show the code that a website will use to interact with the wallet- the
async function connect()
has a check to see if thewindow.ethereum
object is available and if it is then anethereum.request
to connect to one of the accounts in the wallet
- the
"getBalance" button
- checks that metamask is there with the check of
window.ethereum
- ethers package makes it easy to interact and work with wallets/MM
- when we click the "getBalance" button it's making an API call via the RPC url in the wallet
"fund" button
- gets the
ethAmount
from the frontend input box - checks to make sure there is a wallet connected with
window.ethereum
- gets the rpc url using ethers and calls the provider from the wallet
signer
gets the signer object associated with the account to sign the txncontract
uses ethers to get the deployed address of the contract from theconstants.js
file (abi
also comes from here)- REMINDER: You need the
contract address
,abi
, andsigner
to interact with contracts
- REMINDER: You need the
- transaction:
const transactionResponse = await contract.fund(2, "0x0000000000000000000000000000000000000000", {
value: ethers.parseEther(ethAmount),
- creating the txn using javascript and sending to our wallet to sign
- allows the private key to remain in wallet and not be exposed to the website
await
is used since txns are asynchronous and makes sure to wait for the first part of the function to be executed before moving on to the next
cast sig "<function>"()
will reveal the function selector (low level EVM stuff)cast --calldata-decode "<function>(inputs)" <CALLDATA>
will decode the calldata
Order of Layout
- Pragma statements
- Import statements
- Interfaces
- Libraries
- Contracts
Inside of each contract, library or interface, use the following order:
- Type declarations
- State variables
- Events
- Modifiers
- Functions
Order of Functions
- constructor
- receive function (if exists)
- fallback function (if exists)
- external
- public
- internal
- private
Within a grouping, place the
view
andpure
functions last
Helpful to add this at the top of contracts to remember the layout. See
Raffle.sol
for an example.
As of Solidity v0.8.4 there is a new thing called custom errors so you don't have to store the errors as a string which is more expensive
Command + / will comment out a full line
As of Solidity v0.8.26 can add custom errors inside require
but requires
compiling with Via IR that we'll get into more later and still is not as gas
efficient as using if
Version 1
function enterRaffle() public payable {
require(msg.value >= i_entranceFee, "Not enough ETH to enter the raffle");
}
- Explanation: Uses require with a string error message. This is more gas-intensive because the string is stored in the contract’s bytecode, increasing deployment and runtime costs.
Version 2
function enterRaffle() public payable {
require(msg.value >= i_entranceFee, NotEnoughEthToEnterRaffle());
}
- Explanation: Uses require with a custom error. As of Solidity v0.8.26, require statements can accept custom errors. This saves gas by avoiding string literals but requires a newer compiler version.
Version 3
function enterRaffle() public payable {
if (msg.value < i_entranceFee) {
revert NotEnoughEthToEnterRaffle();
}
}
- Explanation: Uses an if statement with revert and a custom error. Available since Solidity v0.8.4, this approach is more gas-efficient because it avoids storing string literals and has less overhead than using require.
It can get confusing on where the error is coming from if you have multiple contracts so best practice is to use the contract name as a prefix with double underscore after
Raffle__NotEnoughEthToEnterRaffle();
- this is a
NotEnoughEthToEnterRaffle
error coming from theRaffle
contract
- this is a
address payable[] private s_players;
is the syntax for making an address array
payable so that you can pay out the winner of the lottery
-
each time someone enters the raffle we can do
s_players.push(payable(msg.sender));
to push that address to the array -
address payable is essential for any address that will receive Ether.
-
Explicit Conversion: Using payable(msg.sender) ensures that the address is correctly cast to a payable type, aligning with Solidity’s type safety.
-
Future-Proofing: Storing addresses as payable prepares your contract for any future functions that may require sending Ether to these addresses.
Why Solidity Still Requires address payable:
-
Type System Enforcement:
- No Assumptions: Solidity does not assume that every address is payable, even if it’s an EOA.
- Explicit Declaration: This enforces intentionality in your code, ensuring that you explicitly handle Ether transfers.
-
Preventing Accidental Transfers:
- Non-Payable Addresses: Using non-payable address types prevents developers from accidentally transferring Ether to unintended recipients.
- Intentional Casting: Developers must intentionally cast an address to address payable when they mean to transfer Ether, adding a layer of intentionality and reducing potential bugs.
Rule of thumb to follow whenever we make updates to storage, emit an event
Why events?
- Makes migration easier
- Makes front end "indexing" easier
EVM can emit logs and inside logs are events
- events allow you to print to the logging structure to save on gas
- BUT events cannot be read by smart contracts, hence the gas savings
Chainlink nodes are listening for request data events on what information it needs to provide The Graph is listening for events and indexes them to be queried later
example:
event storedNumber(
uint256 indexed oldNumber,
uint256 indexed newNumber.
uint256 addedNumber,
address sender
);
indexed
andnonindexed
parameters when you emit events- you can have up to 3
indexed
parameters, also referred to as Topics, are searchable and easier to search for than non-indexed parameters
- you can have up to 3
To emit an event you can do:
emit storedNumber(
favoriteNumber,
_favoriteNumber,
_favvoriteNumber + favoriteNumber,
msg.sender
);
To read the logs on a block explorer like Etherscan
- Address
- the address of the contract or account the event is emitted from
- Topics
- the indexed parameters of the event
- Data
- The ABI encoded non-indexed parameters of the event
- This means we took the non-indexed parameters mashed them together with their ABI and "pumped" them through an encoding algorithm
- If you have the ABI it's easy to decode
- costs less gas to add to the EVM log
- The ABI encoded non-indexed parameters of the event
Globally available unit
- current approximate time according to the blockchain
Example of using block.timestamp
to see if enough time has passed
block.timestamp - s_lastTimeStamp > i_interval;
Can set block.timestamp
in the constructor to have the timestamp be taken as
soon as the contract is created
Subscription used for requesting a random number
- Need to create subscription
- Add funds to your subscription (uses LINK)
- Add consumers
- need to let subscription know about the contracts you are deploying and when
you deploy your contract you need to let it know about your subscription
- Use Subscription ID
VRFCoordinatorV2Interface.sol
used to reach out to the oracle network to get
the random values
keyHash
specifies the gas lane to use which is going to be the premium we are
willing to pay to get faster responses
How VRF works
- makes a request to oracle network
- oracle network generates random numbers
- sends random numbers back to the contract
- when it comes back you need to do something with the numbers as soon as they are returned, they are stored and then become public
callbackGasLimit
max amount of gas that is available to be used in the callback function
- callback function <<<<Look this up, is it the request to the oracle network (the process mentioned above?)>>>>>>>>
requestConfirmations
are the number of block confirmations to pass before
returning the random numbers
numWords
in comp sci is the correct term for what is being returned (we say
random numbers). You can declare the number of "words"/randon numbers are
returned with your request, meaning you can get multiple randon numbers in a
single transaction
function fulfillRandomWords
is the function where you do things with the random values
- example: assigning random traits to an NFT
function getRequestStatus
allows you to see what is going on with the request
- Subscription
- a little more scalable, since you only have to fund this contract vs having to fund ever raffle contract that is deployed
- have a singular subscription contract where you send funds (LINK)
- Direct funding
- directly fund the contract implementing VRF
- example would be if we were to directly fund the
Raffle.sol
contract
- example would be if we were to directly fund the
Setup contract to programmatically get random number
- if you inherit from a contract with a
constructor
you need to add thatconstructor
to your contract VRFConsumerBaseV2Plus.sol
has storage variables_vrfCoordinator
and since we are inheriting, theRaffle.sol
contract also has access to it- The
RandomWordsRequest
is a struct from theVRFV2PlusClient.sol
so when working with a struct you doNAME_OF_CONTRACT.NAME_OF_STRUCT({STRUCT_INPUTS})
VRFV2PlusClient.RandomWordsRequest({
keyHash: s_keyHash,
subId: s_subscriptionId,
requestConfirmations: requestConfirmations,
callbackGasLimit: callbackGasLimit,
numWords: numWords,
extraArgs: VRFV2PlusClient._argsToBytes(
// Set nativePayment to true to pay for VRF requests with Sepolia ETH instead of LINK
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
Merkle trees are a data structure in computer science
- used to encrypt blockchain data more efficiently
- structure
- base: leafs or leaf nodes of the tree that contain has of some data
- top: root or root hash created by hashing all the individual nodes together as leave node hashes
Merkle proof is a way to prove that some data is in one of those leaves to someone who only knows the root hash
- for a successful merkle proof you need to provide all sibling nodes at every tree level
EIP-191 and EIP-712; verifying signatures in contracts
Why do we need them?
- provides more human readable txn messages
- EIP-712 helps prevent replay attacks
- EIP-191 allows for sponsored txns (send txns on behalf of a user given their
signature)
-
0x19<1 byte version><version specific data><data to sign>
0x19
signals that it's a signature- <1 byte version> version that the signed data is using
- 0x00: data with intended validator
- 0x01: structured data (mostly used with EIP-712)
- 0x45: personal_sign messages
- data associated with the version and specified
- data we intend to sign
function getSigner191(uint256 message, uint8 _v, bytes32 _r, bytes32 _s) public view retunrs (address) {} bytes1 prefix = bytes1(0x19); bytes1 eip191Version = bytes1(0); address intendedValidatorAddress = address(this); bytes32 applicationSpecificData = bytes32(message); // ABI encode the data to get a hashed message bytes32 hashedMessage = keccak256(abi.encodePacked(prefix, eip191Version, intendedValidatorAddress, applicationSpecificData)); // use ecrecover precompile with the hashed message and the signature to recover the signer address signer = ecrecover(hashedMessage, _v, _r, _s); return signer }
-
EIP-712 is a standard for typed, structured data signing in Ethereum. It improves upon traditional message signing by:
- Enhancing Readability: It structures the data being signed, making it easier for wallets to display the message content to users.
- Improving Security: By specifying the data format explicitly, it reduces the risk of signing unintended or malicious messages.
The signed message combines structured data with version-specific metadata, creating a deterministic hash that uniquely represents the data.
Key Components of EIP-712 Messages
The EIP-712 signing process involves the following:
The signed message is composed of three parts:
0x19 0x01 < domainSeparator > <hashStruct(message)>
TL;DR 0x19 0x01 <hash of who verifies this signature, and what the verifier looks like> <hash of signed structured message, and what the signature looks like>
- 0x19 0x01:
- A fixed prefix used in EIP-191 (Ethereum’s basic signing scheme).
- Ensures that the signature is interpreted as an EIP-712-compliant structured message and not raw data.
- < domainSeparator >:
- A hash representing the context or domain of the signed message.
- It encodes version-specific metadata, such as the application’s name, version, chain ID, and contract address.
- Ensures that the signed message is valid only within its intended domain, preventing cross-domain replay attacks.
- Structure: Defined as a Solidity struct, typically including:
struct EIP712Domain {
string name; // Name of the dApp
string version; // Version of the dApp
uint256 chainId; // Chain ID (e.g., 1 for Ethereum Mainnet)
address verifyingContract; // Contract address interacting with the user
}
The struct is hashed using the Keccak-256 hash function to produce the domain separator:
domainSeparator = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId,
verifyingContract
));
- <hashStruct(message)>:
- A hash of the structured message to be signed.
- Encodes the actual data being signed (e.g., transaction details or off-chain instructions).
- Ensures that the signing process covers the exact fields and values provided.
- The message fields are defined in a Solidity struct, for example:
struct Message {
string content;
uint256 amount;
}
The struct is hashed in a similar manner to the domain separator:
hashStruct(message) = keccak256(abi.encode(
keccak256("Message(string content,uint256 amount)"),
keccak256(bytes(content)),
amount
));
The final hash to be signed is:
keccak256(abi.encodePacked(
0x19,
0x01,
domainSeparator,
hashStruct(message)
));
This hash ensures:
- Uniqueness: The combination of domain-specific metadata and message content creates a unique identifier for the signed data.
- Compatibility: Wallets can parse and display both the domain and the message fields to users for transparency.
Elliptic Curve Digital Signature Algorithm
Used to:
- generate key pairs
- create signatures
- verify signatures
What are sigantures? (digital fingerprints)
- provide autentication in blockchain tech
- verify that the message (or txn) originates from the intended sender
- uses private key to create the signature
Ethereum address is the 20 bytes of the hash of the public key
Secp256k1 curve used in ECDSA (interoperable with Bitcoin)
- symmetrical about the x axis
- each point on the curve is the
v
,r
,s
and is a unique signature- for every x (
r
) coordinate onthe curve there are 2 valid signatures which could lead to replay- How would this effect Ethereum?
- Ethreum enforces use of a single signature (
s
) value to one half of the curve - Ethereum txns are signed with a hash of the txn so the signature can't change
- Ethereum uses
v
(y coordinate) value
- Ethreum enforces use of a single signature (
- How would this effect Ethereum?
- for every x (
Constants related to the Secp256k1 curve
- Generator Point G:
- random point on the curve, constant point on curve
- Order n:
- prime number generated using G, defines the length of the private key
Private key represented as a random integer between 0 and n-1
Public key = p * G
- p is the private key and it's modular multiplcation with G
How are signatures created?
- hash of the message with the private key using the ECDSA algorithm
- Generate random number R with nonce (k) times Generator point (R = k*G)
- take the x and y coordinate of R
- solve for little r with (r = x mod n)
- calculate s with
s = k^(-1) * (z + r * d) mod n
- k^(-1): mod multiplicative inverse of k modulo n
- d is the private key
- z is the hashed message
- output is (r,s)
How is it verified?
- ECDSA verification algorithm takes the:
- signed message
- signature from the signing algorithm
- public key
Outputs boolean if the recovered signer matches the provided public key
ecrecover precompile does this for you in smart contracts to get the signers address of a message that has been signed using their private key using ECSDA. This allows smart contracts the verify the signature and retrieve the signer
!!! Using ecrecover directly can lead to security issues as signature malleability attacks
Cans use OpenZeppelin's ECSDA library to protect against malleability attacks
!!! if the signature is invalid ecrecover will return the 0 address
- transaction type 113 (0x71 typed structure data)
- EIP-712
- enables access to ZKsync features like account abstraction
- smart contracts must be deployed using a type 113 txn
- added
gasPerPubData
: max gas a sender is willing to pay for a single byte of pubdata (L2 state data that is submitted to the L1)customSignature
: field for the custom signature for when the signer's account is not an EOApaymasterParams
: params for configuring a custom paymasterfactory_deps
: contain the bytecode of the smart contract that is being deployed
- transaction type 5 (0xff priority txns)
- L1 -> L2 txns
- transaction type 0 (Legacy txns)
- what we on Foundry with the
--legacy
flag - txn format before introduction of transaction types
- transaction type 1 (0x01 txns)
- addressed contract breakage risks from EIP-2929
- contains same fields as legacy txns with additional accessList parameter which contains an array of addresses and storage keys
- gas saving on cross-contract calls by predeclaring the allowed contracts and storage slots
- transaction type 2 (0x02 txns)
- EIP-1559
- London Fork
- handles congestion and high fees
- replaced
gasPrice
withbaseFee
- added
maxPriorityFeePerGas
: max fee the user is willing to paymaxFeePerGas
(maxPriorityFeePerGas
+baseFee
): max total fee the sending is willing to pay. How much extra are they willing to pay to have priority plus the base fee
ZKsync does support type 2 txns, but it does nothing with the maxFee parameters
- transaction type 3 (0x03)
- blob txn
- EIP-4844
- Dencun Fork
- scaling solution for rollups
- added
max_blob_fee_per_gas
: max fee per gas the sender is willing to pay for the blob gas. Separate market from regular gas, like max extra fee for the blobsblob_versioned_hashes
: list of the versioned blobbed hashes associated with the txns blobs
- blob fee is deducted and burned before it's executed so failed txns are not refundable
With EOA you need to sign txns with pk, with AA you can sign with ANYTHING (Google account, Github account, etc.)
How does AA work on:
- Ethereum (EntryPoint.sol)
- deploy a smart contract that defines "what" can sign txns
- to send a txn, you send a "UserOp" (user operation) to an Alt Mempool (happens
off-chain)
- UserOp has additional txn information
- Alt-mempool takes the UserOp, validate it, and then send the txn on-chain to
EntryPoint.sol
EntryPoint.sol
does more validation and then routed to your smart contract account so that your account will be themsg.sender
EntryPoint.sol
has some "add-ons"- Signature Aggregator: can define a group of signatures that need to be aggregated
- Pay Master: pay for transactions based on logic setup in your smart contract account
- ZKsync (Native)
- deploy a smart contract where rules are codified (same as Ethereum)
- No Alt-mempool nodes, goes directly to main mempool
- No
EntryPoint.sol
everything goes straight to your contract- This will have the same "add-ons" of Signature Aggregator and Paymaster
All accounts on ZKsync Era are smart contract wallets. DefaultAccount.sol
is
used
- every account even EOAs follow the
IAccount
Interface that includes:validateTransaction
: ZKsync equivalent to EthereumvalidateUserOp
because all txns are txns on ZKsync- send a
_txHash
,_suggestedSignedHash
, and_transaction
(the actual txn itself)
the hyphen (_) before the variable indicates that it's a function input and not a state variable
- the bootloader handles the
_txHash
and_suggestedSignedHash
inputs
- send a
executeTransaction
- executes txn
- can execute a txn with an owner by without going through AA proxy, call directly
- executes txn
executeTransactionFromOutside
- someone else can send your signed tx for you to execute the txn
payForTransaction
- who is going to pay for the txn
prepareForPaymaster
- gets called before you actualy pay a paymaster
MemoryTransactionHelper.sol
helps with the convertions of memory and calldata
to just work with memory.
- inside is the
Transaction
struct that represents a txn on ZKsync (includes things such as txn type, gas information, paymaster, factoryDeps, etc.)- outside of tutoria comees from
TransactionHelper.sol
- outside of tutoria comees from
Can think of magic
as a type of true or false
-
the value that should be equal to the signature of this function if the user agrees to proceed with the txn
if we want it
validateTransaction
to return truereturn IAccount.validateTransaction.selector
- User sends the txn to the "ZKsync API client" (sort of a light node)
- ZKsync API client checks to see the nonce is unique by querying the NonceHolder system contract
- ZKsync API client calls validateTransaction, which MUST update the nonce
whenever you send a type 113 (0x71) txn the msg.sender is the bootloader system contract. Think of it as the entryPoint smart contract on Ethereum in regards to AA
- ZKSync API client checks that the nonce was updated
- ZKsync API client calls payForTransaction, or prepareForPaymster & validateAndPayForPaymasterTransaction (check to make sure there is enough money to pay for the txn)
- ZKsync API client verifies that the bootloader gets paid (user is sending funds to the bootloader so that bootloader can pay to execute the txn)
Validation gets done by the "light node" in order to prevent the main node from getting stuck validating txns and DNS attacks
- ZKsync API client passes the validated txn to the main node / sequencer (currently centralized)
- Main node calls executeTransaction
- If a paymaster was used, the postTransaction is called