DEV Community

Cover image for πŸš€ From Zero to a Web3 Developer πŸ”₯ Solidity & Foundry Bootcamp ⚑ Daily Updates & Code!
MY ZT
MY ZT

Posted on β€’ Edited on β€’ Originally published at dev.to

πŸš€ From Zero to a Web3 Developer πŸ”₯ Solidity & Foundry Bootcamp ⚑ Daily Updates & Code!

This Series of Learning Plans

First and foremost, I want to say thanks to Patrick Collins for this fantastic course: Learn Solidity Smart Contract Development.

This learning plan is based on the outline of his course.

1 Solidity - Remix

  1. β˜… | 2 | Remix - Simple Storage
  2. β˜… | 3 | Storage Factory
  3. β˜… | 4 | Fund Me
  4. β˜… | 5 | AI Prompting

2 Solidity - Foundry

  1. β˜… | 6 | Foundry Simple Storage
  2. β˜… | 7 | Foundry Fund Me
  3. β˜… | 8 | HTML Fund Me
  4. β˜… | 9 | Foundry Smart Contract Lottery

3 Solidity - Advanced Foundry

  1. β˜… | 10 | ERC20s
  2. β˜… | 11 | NFTs
  3. β˜… | 12 | DeFi Stablecoin
  4. β˜… | 13 | Merkle Trees and Signatures
  5. β˜… | 14 | Upgradable Smart Contracts
  6. β˜… | 15 | Account Abstraction
  7. β˜… | 16 | DAOs
  8. β˜… | 17 | Security Introduction

Daily Updates Include:

  • New knowledge encountered in today's study -β†’ πŸ“Œ harvest
  • My practice (if I had done it) -β†’ πŸ“Œ practice
  • Today's learning summary and feelings -β†’ πŸ“Œ sum & F

lesson 2-3

12 Feb:

πŸ“Œ harvest:

πŸ”Ή.public

Person[] public listOfPeople;
Enter fullscreen mode Exit fullscreen mode

"public" keyword can create a getter function automatically.

πŸ”Ή.mapping

mapping(string => uint256) public nameToFavoriteNumber;
nameToFavoriteNumber[_name] = _favoriteNumber;
Enter fullscreen mode Exit fullscreen mode

mapping is like a dictionary in python.

πŸ”Ή.Factory contract

    function createSimpleStorageContract() public {
        SimpleStorage simpleStorageContractVariable = new SimpleStorage();
        // SimpleStorage simpleStorage = new SimpleStorage();
        listOfSimpleStorageContracts.push(simpleStorageContractVariable);
    }
Enter fullscreen mode Exit fullscreen mode

use a Factory contract to create and mange the SimpleStorage contract

πŸ”Ή.inherit

contract AddFiveStorage is SimpleStorage {
    function store(uint256 _favoriteNumber) public override {
        myFavoriteNumber = _favoriteNumber + 5;
    }

contract SimpleStorage {
function store(uint256 _favoriteNumber) public virtual {
    myFavoriteNumber = _favoriteNumber;
}
Enter fullscreen mode Exit fullscreen mode

virtual: Function in the parent contract. If you want the subcontract to be overwritten, you need to add the virtual keyword.
override: The subcontract overwrites the function in the parent contract and requires the override keyword.

πŸ”Ή. data storage
There are three types of Solidity data storage locations: storage, memory, and calldata:

  • storage (contract state variable) : The default state variable in the contract is storage, stored on the chain.
  • memory: Parameters and temporary variables in a function are usually stored in memory and are not chained. In particular, if the returned data type is variable in length, memory modification must be added, such as: string, bytes, array, and custom structures.
  • calldata: Similar to memory, it is stored in memory and is not linked. Unlike memory, the calldata variable is immutable.

πŸ“Œ sum & F:

Actually, I learned solidity basics and install foundry about half a year ago when I was in university, but now I have almost forgotten everything. Now I am graduating and settling down to restart. Today is the first day, and I will persist in studying every day.

Welcome everyone to follow my journey and learn with me!

If you're also interested in Web3 technology, feel free to reach outβ€”we can exchange ideas and grow together!


lesson 4-5

13 Feb:

πŸ“Œ harvest:

πŸ”Ή.oracle
Image description
chain link nodes each reaches out and gets the information about an asset and then signs the data with their own private key in a single transaction then one node will deliver all the data with all the different signatures to a reference contract.

function getPrice() internal view returns (uint256) {
        // Sepolia ETH / USD Address
        // https://docs.chain.link/data-feeds/price-feeds/addresses
        AggregatorV3Interface priceFeed = AggregatorV3Interface(
            0x694AA1769357215DE4FAC081bf1f309aDC325306
        );
        (, int256 answer, , , ) = priceFeed.latestRoundData();
        // ETH/USD rate in 18 digit
        return uint256(answer * 10000000000);
    }
Enter fullscreen mode Exit fullscreen mode

retrieves the latest price of Ethereum (ETH) in USD from a Chainlink price feed and returns the value.

πŸ”Ή.library

// before use library:
getConversionRate(msg.value)

// create a library:
library PriceConverter {}

// use library for uint256:
using PriceConverter for uint256;
msg.value.getConversionRate();
Enter fullscreen mode Exit fullscreen mode

for uint256 This part says we're going to extend the functions in PriceConverter to uint256.
so a uint256 variable can call a PriceConverter function as an object, just like a uint256 method itself. There is no need to explicitly pass parameters.

πŸ”Ή.constant & immutable

uint256 public constant MINIMUM_USD = 50 * 1e18110 ** 18;
//21,415 gas -constant
//23,515 gas -non-constant
//21,415 * 141000000000 = $9.058545
//23,515 * 141000000000 = $9.946845

address public /* immutable */ i_owner;
Enter fullscreen mode Exit fullscreen mode

if you don't use the constant keyword in Solidity, the variable will use storage by default, gas is high not only when deployed, but also when the variable is fetched.

  • constant: Applies to values that are determined at compile time.
  • immutable: applies to variables that are determined at deployment time, but do not change thereafter.

πŸ”Ή.modifier & custom errors

// custom errors
error NotOwner();
// modifier 
    modifier onlyOwner() {
        // require(msg.sender == owner, "not owner");
        // call the error code as opposed to calling the entire string
        if (msg.sender != i_owner) {revert NotOwner(); }
        _;
    }
// use modifier to withdraw
function withdraw() public onlyOwner{}
Enter fullscreen mode Exit fullscreen mode
  • "_;" at the end means execute modifier first, then the part of the function that use this modifier.
  • every characters in this error log needs to get stored individually.so call the error code as opposed to calling the entire string can save gas, since we don't have to store and emit this long string.
  • revert is the same as require.

πŸ”Ή.SendETH

// // transfer
// payable(msg.sender).transfer(address(this).balance);

// // send
// bool sendSuccess = payable(msg.sender).send(address(this).balance);
// require(sendSuccess, "Send failed");

// call
(bool callSuccess,) = payable(msg.sender).call{value: address(this).balance}("");
require(callSuccess, "Call failed");
Enter fullscreen mode Exit fullscreen mode
  • There is no gas limit for call, which is the most flexible and recommended way;
  • The gas limit of transfer is 2300 gas, transaction will be reverted if it fails;
  • The gas limit of send is 2300, the transaction will not be reverted if it fails.

πŸ”Ή.receive & fallback

    fallback() external payable {
        fund();
    }

    receive() external payable {
        fund();
    }

    // Ether is sent to contract
    //      is msg.data empty?
    //          /   \
    //         yes  no
    //         /     \
    //    receive()?  fallback()
    //     /   \
    //   yes   no
    //  /        \
    //receive()  fallback()
Enter fullscreen mode Exit fullscreen mode

πŸ”ΉπŸ”ΉπŸ”Ή.ERC-2335

# create defaultKey
cast wallet import defaultKey --interactive
# see defaultKey
cast wallet list
# here you can find this defaultKey file
cd .foundry/keystores/
# use the defaultKey
forge script script/DeployFundMe.s.so l:DeployFundMe –-account defaultKey --sender 0xf39fd6e51aad88f6f4cΠ΅Π±ab8827279cfffb92266
# delete bash history 
history -c
rm .bash_history
Enter fullscreen mode Exit fullscreen mode

Private key update, use cast provide by foundry.

πŸ“Œ sum & F:

I am looking forward to driving into the study Foundry. It takes me a little time to repair some errors in first using Foundry CLI.
It's useful and important to learn a new and much safer way to hold and use the private key.


lesson 6-7

14 Feb:

πŸ“Œ harvest:

πŸ”Ή.script

    function run() external returns (SimpleStorage) {
        vm.startBroadcast();

        SimpleStorage simpleStorage = new SimpleStorage();

        vm.stopBroadcast();
        return simpleStorage;
    }
Enter fullscreen mode Exit fullscreen mode
forge script script/DeploySimpleStorage.s.sol --private-key <PRIVATE_KEY> --rpc-url <ALCHEMY_URL>
Enter fullscreen mode Exit fullscreen mode

create a script to deploy.

πŸ”Ή.to-base

cast --to-base 0x714c2 dec
Enter fullscreen mode Exit fullscreen mode

Converts a number of one base to another [aliases: --to-base, --to-radix, to-radix, tr, 2r]

πŸ”Ή.cast send & call

cast send 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "store(uint256)" 123 --rpc-url  --private-key 

cast call 0xcf7ed3acca5a467e9e704c703e8d87f634fbofc9 "retrieve()"
0x000000000000000000000000000000000000000000000000000000000000007

cast --to-base 0x000000000000000000000000000000000000000000000000000000000000007b dec
123
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.forge fmt
use forge fmt to format the code.

πŸ”Ή.Transaction types
there are several different types of transactions
Transaction types:

  • Legacy: 0Γ—0
  • Ξ•IP-2930: 0Γ—1
  • Ξ•IP-1559: 0Γ—2
  • Ξ•IP-712: 0Γ—71 // 113
  • Priority: Oxff add the-- Legacy flag it'll send it as a type zero transaction, if don't have the-- Legacy you're probably working with a type two.

πŸ”Ή.install lib

forge install smartcontractkit/chainlink-brownie-contracts --no-commit
remappings = [
    '@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/',
]
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.owner == sender?

contract FundMeTest is Test {
    FundMe fundMe; 
    function setUp() external {
    //us -> FundMeTest -> FundMe
    fundMe = new fundMe(); 
    }

    function testOwnerIsMsgSender() public {
    // assertEq(fundMe.i_owner(), msg.sender);
    assertEq(fundMe.i_owner(), address(this)); 
    }
}
Enter fullscreen mode Exit fullscreen mode

us -> FundMeTest -> FundMe:
message sender is whoever's calling the fundme test.
Actually we deploy the FundMeTest, then FundMeTest deploy the fundMe, so we should be checking to see if fundmetest is the owner.

  • There are two way to complete the test correctly:
  1. use Deploy.s.sol to deploy
import { DeployFundMe} from "../script/DeployFundMe.s.sol"; 
contract FundMeTest is Test {
    FundMe fundMe; 
    function setUp() external {
        DeployFundMe depoyFundMe = new DeployFundMe(); 
        fundMe = deployFundMe.run();
    }
}
Enter fullscreen mode Exit fullscreen mode

When we use the run() function in the DeployFundMe.s.sol into FundMeTest to setup, owner will equal sender correctly.(because DeployFundMe.run() is called directly by us)

  • Values of address(this) and address(msg.sender) when testOwnerIsMsgSender() is run:
  • this: FundMeTest Address of the contract
  • msg.sender: us (Test account assigned by foundry)
  1. Deploy as the deployer
function setUp() public {
    address deployer = makeAddr("deployer"); // Simulate a deployer address
    vm.startPrank(deployer); // Deploy as the deployer
    fundMe = new fundMe(); 
    vm.stopPrank();

    ourToken.transfer(bob, BOB_STARTING_AMOUNT);
    function testOwnerIsMsgSender() public {
    assertEq(fundMe.i_owner(), address(deployer)); 
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.only test one

forge test -m testone 
Enter fullscreen mode Exit fullscreen mode

only test the function testone.

πŸ”Ή.work with addresses onlink

forge test -m testPriceFeedVersionIsAccurate -vvv --fork-url 
$SEPOLIA_RPC_UR
forge coverage --fork-url $SEPOLIA_RPC_URL
Enter fullscreen mode Exit fullscreen mode

when we use a fork URL we're going to simulate what's on that actual chain, it is a great way for us to easily test our contracts on a forked blockchain.

  1. Unit : Testing a specific part of our code(only test FundMe.sol)
  2. Integration : Testing how our code works with other parts of our code(test combined FundMe.sol with deployFundMe.s.sol)
  3. Forked : Testing our code on a simulated real environment
  4. Staging : Testing our code in a real environment that is not prod

πŸ”Ή.modular deployments

AggregatorV3Interface private s_priceFeed; 
constructor(address priceFeed) {
    s_priceFeed = AggregatorV3Interface(priceFeed);
}
Enter fullscreen mode Exit fullscreen mode

To make our deployments more modular with addresses or external systems, we can do a refactoring that pass an address price feed as a Constructor.

πŸ”Ή.Config contract

contract HelperConfig {
    // If we are on a local anvil, we deploy mocks
    // Otherwise, grab the existing address from the live network
    NetworkConfig public activeNetworkConfig;

    struct NetworkConfig {
        address priceFeed;
    }

    // ETH/USD price feed address
    constructor() {
        if (block.chainid == 11155111) {
            activeNetworkConfig = getSepoliaEthConfig();
        } else {
            activeNetworkConfig = getAnvilEthConfig();
        }
    }

    function getSepoliaEthConfig() public pure returns (NetworkConfig memory) {
        // Price feed address
        NetworkConfig memory sepoliaConfig = NetworkConfig({
            priceFeed: 0x694AA1769357215DE4FAC081bf1f309aDC325306
        });
        return sepoliaConfig;
    }

    function getAnvilEthConfig() public pure returns (NetworkConfig memory) {
        // Price feed address
    }
}
Enter fullscreen mode Exit fullscreen mode
  • This is a strategy we can work with different chains.
  • Why use a struct instead of Assign a value to activeNetworkConfig directly? Because Solidity contracts may constantly expand, Use struct for better scalability.

πŸ”Ή.Before startBroadcast

contract DeployFundMe is Script {
    function run() external returns (FundMe) {
        // Before startBroadcast -> Not a "real" tx
        HelperConfig helperConfig = new HelperConfig();
        address ethUsdPriceFeed = helperConfig.activeNetworkConfig();

        // After startBroadcast -> Real tx!
        vm.startBroadcast();
        FundMe fundMe = new FundMe(ethUsdPriceFeed);
        vm.stopBroadcast();

        return fundMe;
    }
}
Enter fullscreen mode Exit fullscreen mode

When we want to write a contract to implement some configuration functions, we can new and invoke it before startBroadcast.

πŸ“Œ sum & F:

At noon, I spent some time installing WSL on my Windows 10. Fortunately, the installation was successful.
Today, I learned the basic test of foundry and learned about Script,test and config contracts.


lesson 7

15 Feb:

πŸ“Œ harvest:

πŸ”Ή.mock

    function getAnvilEthConfig() public returns (NetworkConfig memory) {
        uint8 public constant DECIMALS = 8;
        int256 public constant INITIAL_PRICE = 2000e8;
       // Check to see if we set an active network config
        if (localNetworkConfig.priceFeed != address(0)) {
            return localNetworkConfig;
        }

        // Price feed address
        vm.startBroadcast();
        MockV3Aggregator mockPriceFeed = new MockV3Aggregator(DECIMALS, INITIAL_PRICE);
        vm.stopBroadcast();

        localNetworkConfig = NetworkConfig({priceFeed: address(mockPriceFeed)});
        return localNetworkConfig;
    }
Enter fullscreen mode Exit fullscreen mode

mkdir -p test/mock && touch test/mock/MockV3Aggregator.sol, then new MockV3Aggregator at HelperConfig.s.sol to deploy a mock contract to have our own fake price feed on Anvil.
mock much faster than fork, we didn't have to make it any API calls.

πŸ”Ή.vm.expectRevert

    function testFundFailsWithoutEnoughETH() public{
        vm.expectRevert();
        fundMe.fund(); // send 0 eth
    }
Enter fullscreen mode Exit fullscreen mode

The next line should revert.

πŸ”Ή.private

mapping(address => uint256) private s_addressToAmountFunded;

function getAddressToAmountFunded(address fundingAddress) public view returns (uint256) {
    return s_addressToAmountFunded[fundingAddress];
}
Enter fullscreen mode Exit fullscreen mode

private variables are more gas efficient so we just want to default them to private and then only make public or external view function (getter) as we need.

πŸ”Ή.makeAddr & deal

FundMe fundMe;
address USER = makeAddr("user");
uint256 constant SEND_VALUE = 0.1 ether;
uint256 constant STARTING_BALANCE = 10 ether;

function setUp() external {
    DeployFundMe deployFundMe = new DeployFundMe();
    fundMe = deployFundMe.run();
    vm.deal(USER, STARTING_BALANCE);
}
Enter fullscreen mode Exit fullscreen mode
  • makeAddr(from forge-std) Creates an address derived from the provided name.
  • vm.deal Sets the balance of an address who to newBalance.

πŸ”Ή.vm.prank

    function testFundUpdatesFundedDataStructure() public {
        vm. prank(USER); //The next TX will be sent by USER fundMe. 
        fund{value:SEND_VALUE}(); 
        uint256 amountFunded = fundMe.getAddressToAmountFunded(USER); 
        assertEq(amountFunded, SEND_VALUE);
    }
vm.startPrank(fundMe. getOwner());
fundMe.withdraw(); 
vm.stopPrank();
Enter fullscreen mode Exit fullscreen mode

knowing who is doing what can be a little bit confusing especially in our tests, so in our test we want to be very explicit with who's sending what transactions and that's where we can use another foundary cheatcode called prank.

πŸ”Ή.test with modifier

modifier funded() {
    vm.prank(USER);
    fundMe.fund{ value: SEND_VALUE}();
    _;
}
function testOnlyOwnerCanWithdraw() public funded {
    vm.prank(USER);
    vm.expectRevert(); 
    fundMe.withdraw();
}
Enter fullscreen mode Exit fullscreen mode

When you call payable (ETH payable) function, ETH can be appended using the {value: amount} syntax.

πŸ”Ή.test pattern

  • Arrange: first I'm going to arrange the test I'm
  • Act: then I'm going to do the action I actually want to test
  • Assert: then I'm going to assert the test

πŸ”Ή.uint160

uint256 i = uint256(uint160(msg. sender));
uint160 k = startingFunderIndex;
address (k)
Enter fullscreen mode Exit fullscreen mode

As of Solidity v0.8, you can no longer cast explicitly from address to uint256.

The reason for this is a uint160 has the same amount of bytes essentially as an address. So if you want to use numbers to generate addresses those numbers have to be a uint160.

πŸ”Ή.hoax

// vm.prank new address
// vm.deal new address
hoax(address(i), SEND_VALUE);
Enter fullscreen mode Exit fullscreen mode

Sets up a prank from an address that has some ether. It does both prank and deal combined.

πŸ”Ή.gas-snapshot

forge snapshot -m testWithdrawFromMultipleFunders
Enter fullscreen mode Exit fullscreen mode

It's going to create this file called gas snapshot for us and tell us exactly how much gas this single test costs.

πŸ”Ή.vm.txGasPrice

//Act 
uint256 public constant GAS_PRICE = 1;

uint256 gasStart = gasleft(); //1000
vm.txGasPrice(GAS_PRICE); 
vm.prank(fundMe.getowner()); //c: 200
fundMe.withdraw(); //should have spent gas?

uint256 gasEnd = gasleft(); //800
uint256 gasUsed = (gasStart -gasEnd) * tx.gasprice;
console.log(gasUsed);
Enter fullscreen mode Exit fullscreen mode
  • when you're working with Anvil the gas price actually defaults to zero(whether forked or not).
  • gas left function is a built-in function in solidity it tells you how much gas is left in your transaction call.
  • tx. gas price is built-in to solidity which tells you the current gas price

πŸ”Ή.Storage
Image description

for array it does have a storage slot for the length, for mappings it does have a storage spot as well similar to array, but it's just blank intentionally so that solidity knows ah okay there is a mapping here.

cast storage ADDRESS <slot index>
Enter fullscreen mode Exit fullscreen mode

Image description

πŸ”Ή.integration tests

  • integration tests are when you test a lot of your interactions and combinations of systems.
  • is going to be testing all of our deploy scripts and how all of our contracts interact with each other.
  • At FundMeTest.s.sol we tested funding and withdrawing, but we didn't test funding and withdrawing using the methods that we're actually going to use to do that when we actually call funds and withdraws we're do it with Forge script.
  • integration tests is going to have a programmatic way to do it with Forge script so that we can have a reproducible way to actually interact with fund and withdraw.it simulated real user interaction.

πŸ”Ή.get_most_recent_deployment

    function run() external {
        address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment("FundMe", block.chainid);
        fundFundMe(mostRecentlyDeployed);
    }
Enter fullscreen mode Exit fullscreen mode

Has this devops tool to get the most recent deployment that we can use to get the most recently deployed version of a contract, this way we don't have to pass the fundme contract address that we want to work with every single time.(because when we run β€œforge script”, function run() is must implemented in our script).

πŸ“Œ sum & F:

Today, I learned a lot about foundry testing, such as mock and integration. Although Patrick's tutorial only watched 1 and a half hours of video, I felt that I learned so much today that I gave up watching the tutorial tonight. Instead, I will try to run what I learned today in my own vscode and finally pass all the tests.


lesson 7-9

16 Feb:

πŸ“Œ harvest:

πŸ”Ή.window.ethereum

if (typeof window.ethereum !== "undefined") {
    const provider = new ethers.BrowserProvider(window.ethereum)
    await provider.send('eth_requestAccounts', [])
  } else {
    withdrawButton.innerHTML = "Please install MetaMask"
  }
Enter fullscreen mode Exit fullscreen mode

It's this window.ethereum JavaScript object that these websites interact with to send transactions to our metamask.

πŸ”Ή.function selectors

cast sig "fund()" 
0xb60d4288
# this is the function selector
Enter fullscreen mode Exit fullscreen mode

solidity functions get transformed into this thing called a function selector, when calling this fund function is it converts it to its function selector

πŸ”Ή.Solidity Layout

  • Layout of Contract:
    version
    imports
    errors
    interfaces, libraries, contracts
    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

πŸ”Ή.custom error

// require(msg. value >= i_entranceFee, "Not enough ETH sent!");
// 0.8.26 :
// require(msg. value >= i_entranceFee, SendMoreToEnterRaffle());
if (msg. value < i_entranceFee) {
    revert Raffle_SendMoreToEnterRaffle();
}
Enter fullscreen mode Exit fullscreen mode

The last is the most gas efficient out of all these methodologies.

πŸ”Ή.payable addrass

address public notPayableAddress;
address payable public payableAddress;
// send ETH to address and it's not payable, the transaction will fail
// notPayableAddress.transfer(1 ether);
payable(notPayableAddress).transfer(1 ether); 
Enter fullscreen mode Exit fullscreen mode

which can receive ETH ?

  • address : no
  • address payable : yes
  • contract(no receive or fallback) : no
  • contract(have receive()) : yes

πŸ”Ή.event

Image description

  • (Indexed Parameters = Topics) we can have up to three indexed parameters. Topics are searchable.
event storedNumber(
    uint256 indexed oldNumber,
    uint256 indexed newNumber, 
    uint256 addedNumber, 
    address sender
};

emit storedNumber(
    favoriteNumber,
    favoriteNumber,
    favoriteNumber + favoriteNumber,
    msg.sender
);
Enter fullscreen mode Exit fullscreen mode
  • whenever we update something in storage and this rule of thumb is going to always be emitting an event whenever we update a storage variable. the two main reasons that work with events:
  1. Makes migration easier : it can be very difficult to move all the storage to the new contract.
  2. Makes front end "indexing" easier.

πŸ”Ή.block.timestamp
it's going to be the current approximate time of according to the blockchain.

πŸ“Œ sum & F:

Today I reviewed the Foundry Fund Me code and uploaded it to GitHub, watched Patrick demonstrate metamask's interaction with the site, and pulled the code and tried it out myself. Finally, start learning Smart Contract Lottery.


lesson 9

17 Feb:

πŸ“Œ harvest:

πŸ”Ή.verifiable Randomness function (vrf)

    function pickWinner() external {
        // Check to see if enough time has passed
        if ((block.timestamp - s_lastTimeStamp) < i_interval) {
            revert();
        }
        // Fill in parameter
        VRFV2PlusClient.RandomWordsRequest memory request = VRFV2PlusClient.RandomWordsRequest({
            keyHash: i_keyHash,
            subId: i_subscriptionId,
            requestConfirmations: REQUEST_CONFIRMATIONS,
            callbackGasLimit: i_callbackGasLimit,
            numWords: NUM_WORDS,
            extraArgs: VRFV2PlusClient._argsToBytes(
                // Set nativePayment to true to pay for VRF requests with Sepolia ETH instead of LINK
                VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
            )
        });
        // Request Chainlink VRF Oracle.
        // The oracle submits random numbers and proofs to the Chainlink VRF Coordinator contract.
        // Chainlink VRF generates the unique identifier requestId for this request
        uint256 requestId = s_vrfCoordinator.requestRandomWords(request);

    }

    // The Chainlink VRF Coordinator calls the rawFulfillRandomWords of your contract after the oracle returns the data.
    function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal virtual override {}
Enter fullscreen mode Exit fullscreen mode

process:

  • Fill in parameter.
  • Request Chainlink VRF Oracle.
  • The oracle submits random numbers and proofs to the Chainlink VRF Coordinator contract.
  • Chainlink VRF generates the unique identifier requestId for this request
  • The Chainlink VRF Coordinator calls the rawFulfillRandomWords of your contract after the oracle returns the data.

πŸ”Ή.safety design

function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal virtual;
function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) external {
    if (msg.sender != address(s_vrfCoordinator)) {
        revert OnlyCoordinatorCanFulfill(msg.sender, address(s_vrfCoordinator));
    }
    fulfillRandomWords(requestId, randomWords);
}
Enter fullscreen mode Exit fullscreen mode
  • rawFulfillRandomWords ensure the separation of security checks and business logic.
  • fulfillRandomWords are internal and cannot be called directly from the outside.Outsiders can only call rawFulfillRandomWords that have been checked for security.
  • Why not just allow subcontracts to implement rawFulfillRandomWords directly? The subcontract may forget or intentionally delete msg.sender validation.

πŸ”Ή.inherit Constructor

// contract VRFConsumerBaseV2Plus:
  IVRFCoordinatorV2Plus public s_vrfCoordinator;

  constructor(address _vrfCoordinator) ConfirmedOwner(msg.sender) {
    if (_vrfCoordinator == address(0)) {
      revert ZeroAddress();
    }
    s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator);
  }
// contract Raffle 
contract Raffle is VRFConsumerBaseV2Plus{
    constructor(
        uint256 entranceFee,
        uint256 interval,
        address vrfCoordinator
    ) VRFConsumerBaseV2Plus(vrfCoordinator) {
        i_entranceFee = entranceFee;
        i_interval = interval;
        s_lastTimeStamp = block.timestamp;
    }
     uint256 requestId = s_vrfCoordinator.requestRandomWords(request);
}
Enter fullscreen mode Exit fullscreen mode

if we we use the Constructor of the inherited contract then we need to add that inherited contract's Constructor. We're inheriting so we get access to these State variables too.

πŸ”Ή.abstract contract

function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal virtual;
// implement:
function fulfillRandomWords(uint256, /* requestId */ uint256[] calldata randomWords) internal override {}
Enter fullscreen mode Exit fullscreen mode

contract abstract contracts can have both undefined functions [virtual] and defined functions. inherit an abstract contract must Implement functions that it undefined [override].

πŸ”Ή.Enum

    enum RaffleState {
        OPEN,    // 0
        CALCULATING    // 1
    }
    s_raffleState = RaffleState.OPEN;

uint256(s_raffleState);
Enter fullscreen mode Exit fullscreen mode
  • keep track of the raffle and figure out are we in the middle of calculating the winner.
  • each one of these parameters inside of enum get mapped to a un 256.

πŸ”ΉπŸ”ΉπŸ”Ή.CEI Patten

keep checks effects interactions in mind that will help you just be safer by default (defend against re-entrancy attacks).

  1. Checks (conditionals)
  2. Effect (Internal Contract State and emit Event)
  3. Interactions (External Contract Interactions)

πŸ”Ή.calldata

  • calldata is part of the EVM transaction calldata and is stored directly in the input data of the transaction call, not in memory within Solidity code.
  • The calldata variable is immutable and is used only as an argument to external functions, in order to save gas and avoid copying data to memory.
  • internal functions cannot use calldata, only memory.

πŸ”Ή.pure

function hashData(string memory data) public pure returns (bytes32) {
    return keccak256(abi.encodePacked(data));  // Calculates the hash without modifying the contract state
}
Enter fullscreen mode Exit fullscreen mode

pure is suitable for:

  • βœ” Pure mathematical operations
  • βœ” Handle strings, arrays, and do not access contract storage
  • βœ” Calculate the hash value
  • βœ” Check input data

cannot be used pure:

  • ❌ Access state variables
  • ❌ Modifies the stored variable
  • ❌ Access global variables such as msg.sender, block.timestamp
  • ❌ calls a function of view or non-pure

πŸ”Ή.when use memory&calldata

// The base type uint256 is used as the argument and return value and does not require a storage location
function add(uint256 a, uint256 b) public pure returns (uint256) {
    return a + b;
}

// Passing the string argument must specify either memory or calldata
function greet(string memory name) public pure returns (string memory) {
    return string(abi.encodePacked("Hello, ", name, "!"));
}
Enter fullscreen mode Exit fullscreen mode

when require a storage location in parameter and return?

  • uint256、bool、address❌
  • string、struct、array βœ”

πŸ”Ή.Mock & Fork
Whether VRF contract exists? Mock is needed? VRF predictor responds?

  • Local Anvil : ❌ βœ” ❌
  • Forked Sepolia : βœ” βœ” ❌

On Forked Sepolia, even with a real VRF contract, the oracle won't respond to Fork network requests, so you'll still need Mock.

πŸ”Ή.Test Event

function testEnteringRaffleEmitsEvent() public {
    // Arrange
    vm.prank(PLAYER);
    // Act
    vm.expectEmit(true, false, false, false, address(raffle));
    emit RaffleEntered(address(PLAYER));
    // Assert
    raffle.enterRaffle{value: entranceFee}();
}
Enter fullscreen mode Exit fullscreen mode
  • It's the first index parameter, so we are going to say true, there are no additional index parameters (up to 3), and there are also no non-indexed parameters, so we're going to say false, raffle address is actually going to be emitting.
  • We have to copy paste our events into the top of our test.

πŸ”Ή.warp & roll

// set time passed
vm.warp(block. timestamp + interval + 1); 
// set the blocks increased.
vm.roll(block. number + 1);
Enter fullscreen mode Exit fullscreen mode

Time has passed and the blocks have actually increased.

πŸ“Œ sum & F:

Today I learned vrf, knowing how to get a random number and implemented it basically. Then make a HelperConfig.s.sol to set config for different networks. Starting in the evening, studying for the Raffle Test. I had the pleasure of reviewing a lot of solidity's basic syntax while implementing Raffle.


lesson 9

18 Feb:

πŸ“Œ harvest:

πŸ”Ή.function selector databases

Image description

function signature or function selector
openchain.xyz/signatu res

πŸ”Ή.import

// deploy VRFCoordinatorV2_5Mock contract
vm.startBroadcast();
VRFCoordinatorV2_5Mock vrfCoordinator = new VRFCoordinatorV2_5Mock(MOCK_BASE_FEE, MOCK_GAS_PRICE_LINK, MOCK_WEI_PER_UINT_LINK);
vm.stopBroadcast();

// invoke VRFCoordinatorV2_5Mock function in another contract
import {VRFCoordinatorV2_5Mock}
VRFCoordinatorV2_5Mock(vrfCoordinator).createSubscription();
Enter fullscreen mode Exit fullscreen mode
  • "import" only imports the ABI of the contract.
  • The cast VRFCoordinatorV2_5Mock(vrfCoordinator) simply tells the compiler that the address matches the ABI, Solidity will call the function on chain use the ABI.

πŸ”Ή.Create Subscription

  • mock VRFCoordinatorV2_5Mock.sol form chainlink.
  • Create a helperconfig.s.sol to configure NetworkConfig for sepolia and deploy a mock VRF contract for local chain.
  • Create an interactions.s.sol to implement the interaction logic speciality.
function createSubscription(
    address vrfCoordinator
) public returns (uint256, address) {
    vm.startBroadcast();
    uint256 subId = VRFCoordinatorV2_5Mock(vrfCoordinator)
        .createSubscription();
    vm.stopBroadcast();

    return (subId, vrfCoordinator);
}
Enter fullscreen mode Exit fullscreen mode
  • new CreateSubscription in DeployRaffle.s.sol:
if (config.subscriptionId == 0) {
    // create subscription
    CreateSubscription createSubscription = new CreateSubscription();
    (config.subscriptionId, config.vrfCoordinator) =
        createSubscription.createSubscription(config.vrfCoordinator);
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.Fund Subscription

  • mock LinkToken.sol form chainlink.
  • import ERC20.sol:
forge install transmissions11/solmate@v6 --no-commit 
Enter fullscreen mode Exit fullscreen mode

add remappings =['@solmate=lib/solmate/src/'] in foundry.toml.

import {ERC20} from "@solmate/tokens/ERC20.sol";
Enter fullscreen mode Exit fullscreen mode
  • add mint function:
function mint(address to, uint256 value) public {
    _mint(to, value);
}
Enter fullscreen mode Exit fullscreen mode
  • deploy LinkToken in HelperConfig.s.sol, and use it on Anvil NetworkConfig.
  • create contract FundSubscription in interactions.s.sol and write a fundSubscription to fund for subscriptionId:
function fundSubscription(
        address vrfCoordinator,
        uint256 subscriptionId,
        address linkToken
    ) public {

        if (block.chainid == LOCAL_CHAIN_ID) {
            vm.startBroadcast();
            // this fundSubscription is provide by VRFCoordinatorV2_5Mock.sol specially
            VRFCoordinatorV2_5Mock(vrfCoordinator).fundSubscription(
                subscriptionId,
                FUND_AMOUNT
            );
            vm.stopBroadcast();
        } else {
            vm.startBroadcast();
            LinkToken(linkToken).transferAndCall(
                vrfCoordinator,
                FUND_AMOUNT,
                abi.encode(subscriptionId)
            );
            vm.stopBroadcast();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • new FundSubscription and fund subscriptionId in DeployRaffle.s.sol
//  Found it!
FundSubscription fundSubscription = new FundSubscription();
fundSubscription.fundSubscription(
    config.vrfCoordinator, config.subscriptionId, config.link
);
Enter fullscreen mode Exit fullscreen mode
  • use FundSubscription to fund on sepolia:
forge script script/Interactions.s.sol:FundSubscription --rpc-url https://eth-sepolia.g.alchemy.com/v2/xxx --private-key xxxx --broadcast

Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.Add Consumner

  • forge install Cyfrin/foundry-devops --no-commit
address mostRecentlyDeployed = DevOpsTools. get_most_recent_deployment("Raffle", block. chainid);
Enter fullscreen mode Exit fullscreen mode
  • Create contract AddConsumer in interactions.s.sol and write an addConsumer to add the most recent deployment contract Raffle for the Consumer.
function addConsumer(address contractToAddToVrf, address vrfCoordinator, uint256 subId) public {
    console.log("Adding consumer contract: ", contractToAddToVrf);
    console.log("To vrfCoordinator: ", vrfCoordinator);
    console.log("On ChainId: ", block.chainid);
    vm.startBroadcast();
    VRFCoordinatorV2_5Mock(vrfCoordinator).addConsumer(subId, contractToAddToVrf);
    vm.stopBroadcast();
}
Enter fullscreen mode Exit fullscreen mode
  • new AddConsumer in DeployRaffle.s.sol:
AddConsumer addConsumer = new AddConsumer();
//have broadcast in AddConsumer
addConsumer.addConsumer(address(raffle), config. vrfCoordinator, config. subscriptionId); 
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.coverage report----t=65086

forge coverage --report debug > coverage.txt
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.recordLogs

vm.recordLogs();
raffle.performUpkeep("");
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 requestId = entries[1].topics[1];
Enter fullscreen mode Exit fullscreen mode

whatever events whatever logs are emitted by this performupkeep
function, keep track of those and stick them into an array.

πŸ”Ή.fuzz testing

[fuzz]
runs = 1000
# testFulfillrandomWordsCanOnlyBeCalledAfterPerformUpkeep(uint256) (runs: 1000)
Enter fullscreen mode Exit fullscreen mode

Test try to break the code.

πŸ“Œ sum & F:

Today I followed Patrick done the Subscription create and found, add Consumner. Found subscription on sepolia. Do some tests to the raffle contract.


lesson 9-10

19 Feb:

πŸ“Œ harvest:

πŸ”Ή.About test fulfillRandomWords

// vrfCoordinator is the deploy address of vrfCoordinatorV2_5Mock in HelperConfig.s.sol
VRFCoordinatorV2_5Mock(vrfCoordinator).fulfillRandomWords(uint256(requestId), address(raffle));
Enter fullscreen mode Exit fullscreen mode
  • Why can this invoke the fulfillRandomWords at Raffle?
// VRFCoordinatorV2_5Mock.sol :
   bytes memory callReq = abi.encodeWithSelector(v.rawFulfillRandomWords.selector, _requestId, _words);
    s_config.reentrancyLock = true;
    // solhint-disable-next-line avoid-low-level-calls, no-unused-vars
    (bool success, ) = _consumer.call{gas: req.callbackGasLimit}(callReq);
Enter fullscreen mode Exit fullscreen mode

VRFCoordinatorV2_5Mock is just an intermediary that triggers Raffle's fulfillRandomWords method with a low-level call

  • When testing the fulfillRandomWords, why don't invoke fulfillRandomWords at Raffle directly ?
  1. fulfillRandomWords is internal.
  2. fulfillRandomWords is the callback function, which must be called by

πŸ”Ή.default caller address

// lib > forge-std > src > Base.sol
abstract contract CommonBase {

//Default address for tx. origin and msg. sender, 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
address internal constant DEFAULT_SENDER = address(uint160(uint256(keccak256("foundry default caller"))));
...
}
Enter fullscreen mode Exit fullscreen mode

whenever Foundry needs to use some type of address to send some stuff this is the address that it's going to use.So in helperconfig we can set the local network default account to being this.

πŸ”Ή.Fork tests

    // fulfillRandomWords is only can be called by vrf, so at fork network we skip it
    modifier skipFork() {
        if (block.chainid != 31337) {
            return;
        }
        _;
    }
Enter fullscreen mode Exit fullscreen mode

The actual chain link coordinator have access controls and only let the chain link nodes call fulfillrandomwords.
we're going to skip those two that don't make sense for us to to Fork.

πŸ”Ή.startBroadcast(address)

function startBroadcast() external; 
function startBroadcast(address who) external; 
function startBroadcast(uint256 privatekey) external;

// Use the address to broadcast things.
vm.startBroadcast(config.account);
Raffle raffle = new Raffle(
);
vm.stopBroadcast();
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.stages, scope and type of tests

  • unit
  • integrations
  • forked
  • staging <- run tests on a mainnet or testnet

  • fuzzing

  • stateful fuzz

  • stateless fuzz

  • formal verification <- turn your code into mathematical proofs

πŸ”Ή.verify-contract

forge verify-contract 0xA7Deda6D9a52A43E39C655453C2F7b3a203Da671 src/Raffle. sol: Raffle --etherscan-api-key $ETHERSCAN_API_KEY --rpc-url $SEPOLIA_RPC_URL --show-standard-json-input > json.json
Enter fullscreen mode Exit fullscreen mode

if for some reason your code doesn't automatically update your code doesn't automatically verify this is how to do. --> [t=69099]

πŸ”Ή.ERC20Token

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract OurToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("OurToken", "OT") {
        _mint(msg.sender, initialSupply);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.transferFrom

function _spendAllowance(
    address owner, 
    address spender, 
    uint256 amount
) internal virtual {
    // Gets the amount of spender currently authorized by the owner
    // allowance is record by mapping(address => mapping(address => uint256)) _allowances;
    uint256 currentAllowance = allowance(owner, spender);  
    require(currentAllowance >= amount, "ERC20: insufficient allowance");

    unchecked {
        if (currentAllowance != type(uint256).max) {
            // Renewal of authorization
            _approve(owner, spender, currentAllowance - amount);
        }
    }
}

function transferFrom(
    address from,
    address to,
    uint256 amount
) public virtual override returns (bool) {
    _spendAllowance(from, msg.sender, amount);  // Consumption authorization
    _transfer(from, to, amount);
    return true;
}

Enter fullscreen mode Exit fullscreen mode

transferFrom() use _spendAllowance() to check and consume the authorized amount, use _transfer() to do transfer.

πŸ“Œ sum & F:

Finished a learning raffle course, passed all the tests and deployed to sepolia, pushed to GitHub. learned Foundry ERC20s, did some tests with the help of AI.Drive into Advanced Foundry.


lesson 11

20 Feb:

πŸ“Œ harvest:

πŸ”Ή.IPFS

  • IPFS drastically different than a blockchain is they din't have execution it's just decentralized storage.
  • Unlike a blockchain where every single node in a blockchain is going to have a copy of the entire blockchain ipfs nodes get to optionally choose which data they want to pin.

  • Pinata is an IPFS third-party hosting service, we can upload files to IPFS without having to run IPFS nodes.

  • Filecoin and arweave are two decentralized storage platforms that we could alternatively use instead of ipfs.

πŸ”Ή.compare stirngs

string memory expectedName = "Dogie"; 
string memory actualName = basicNft.name();
//assert(expectedName == actualName); assert(keccak256(abi.encodePacked(expectedName)) == keccak256(abi.encodePacked(actualName));
Enter fullscreen mode Exit fullscreen mode

Compare the two of them we would just compare the hashes instead of looping through the array.
we can convert our strings to bytes and convert the bytes to a byes 32 and we just compare the hashes of both of those.

πŸ”Ή.SVG NFT

  • Store NFT in Scalable Vector Graphics (SVG) format directly in a smart contract, never lose.
  • NFT has the ability to change dynamically.

πŸ”Ή.differences between a unit test and an integration test-->[t=78423]

  1. Unit : Testing a specific part of a contract.(only test FundMe.sol)
  2. Integration : Testing how our contracts works with each other.(test combined FundMe.sol with deployFundMe.s.sol)

πŸ“Œ sum & F:

Learned ERC721, IPFS storage for NFT (test on sepolia), SVG format NFT for full on-chain storage (test on anvil, sepolia is insufficient funds for gas).spent some time on unit testing.


lesson 11

21 Feb:

πŸ“Œ harvest:

πŸ”Ή.encode->[t=79079]

  1. abi.encode("string")
  2. bi.encodePacked("string")
  3. bytes("string")
  • abi.encodePacked() it's the encode function but it compresses stuff if we wanted to encode some string but we wanted to save space and we didn't need the perfect low-level binary of it.
  • EncodePacked and bytes get the same result.

  • Dncode:

  1. abi.decode(encodeString(), (string))
  2. string(EncodePacked()

πŸ”Ή.call

// Example Function Selector:
0xa9059cbb 
// Example Function Signature:
"transfer(address, uint256)"
Enter fullscreen mode Exit fullscreen mode
  • The Function Selector for each function is the hash of its first 4 bytes. bytes4(keccak256(bytes("transfer(address,uint256)")));
    // use selector and calldata to call a function
    function callTransferFunctionDirectly(address someAddress, uint256 amount) public returns (bytes4, bool) {
        (bool success, bytes memory returnData) = address(this).call(
            // getDataToCallTransfer(someAddress, amount);
            abi.encodeWithSelector(getSelectorOne(), someAddress, amount)
        );
        return (bytes4(returnData), success);
    }
Enter fullscreen mode Exit fullscreen mode

call calls only the methods of the target contract based on the ABI encoding (Function Selector + Arguments Encoding).

πŸ”Ή.stable coin

  • Classification of stable coin

Algorithmic stable coins use some sort of autonomous permissionless code to Mint and burn tokens whereas a govern stable coin have some human interaction that mints and burns the coins and keeps them stable.

  • Classification of collateral:
  1. exogenous collateral is collateral that originates from outside the protocol
  2. endogenous collateral originates from inside the protocol
  3. one of the easier ways to Define what type of collateral protocol is using is to ask this question if the stable coin fails does the underlying collateral also fail if yes it's endogenous if no it's exogenous. exogenously collateralized stable coins are typically over collateralized.
  4. was the collateral created with the sole purpose of being collateral or does the protocol own the issuance of the underlying collateral if the answer is yes to either one of those then it's endogenous collateral
  • RAI:

so protocols like RAI have come a long way to do some type of hybrid between endogenous and exogenously collateralized stable coin.

  • who is minting these stable coins and pay Stability Fee?

people who want to own more eth. (maximize your position)
put eth into one of these stable coin protocols minted stable coin and then sell the stable coin for more eth.

πŸ“Œ sum & F:

Today I learned abi.encode,call function with Selector, and have a basic understanding of stablecoins.Start to learn the building of stablecoins project.


lesson 12

22 Feb:

πŸ“Œ harvest:

πŸ”Ή.address cast

// βœ… tokenCollateralAddress inherit IERC20
IERC20(tokenCollateralAddress)  
// ❌ tokenCollateralAddress inherit ERC20, But the contract  of tokenCollateralAddress itself is not ERC20
ERC20(tokenCollateralAddress)   
// βœ… The contract  of vrfCoordinator itself is VRFCoordinatorV2_5Mock, vrfCoordinator address points to a VRFCoordinatorV2_5Mock contract that has been deployed
VRFCoordinatorV2_5Mock(vrfCoordinator)  
Enter fullscreen mode Exit fullscreen mode

Convert the address to a contract interface.

πŸ”Ή.Reentrant

function withdraw(uint256 amount) external {
    require(balance[msg.sender] >= amount, "Not enough balance"); 
    // ⚠️ A reentrant attack may occur here
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
    balance[msg.sender] -= amount; 
}

    // contract Attacker, call withdraw again when ETH is received.
    receive() external payable {
        if (address(target).balance > 0) {
            target.withdraw(1 ether);
        }

// use ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureContract is ReentrancyGuard {
    function withdraw(uint256 amount) external nonReentrant {}
}
Enter fullscreen mode Exit fullscreen mode
  • nonReentrant is required for any function that involves an external contract interaction and may cause a change in funds.
  • If the contract changes its state before invoking the external contract, it can effectively prevent reentrant attacks.

πŸ”Ή.burnDsc

function _burnDsc(uint256 amountDscToBurn, address onBehalfOf, address dscFrom) private {
    s_DSCMinted[onBehalfOf] -= amountDscToBurn;
    // user approve this,this invoke the transferFrom of dsc through ABI to transfer token to this
    // because only the token holder can call the burn function
    bool success = i_dsc.transferFrom(dscFrom, address(this), amountDscToBurn);
    // This conditional is hypothetically unreachable
    if (!success) {
        revert DSCEngine__TransferFailed();
    }
    // now the token owner is this ,this can call the burn
    i_dsc.burn(amountDscToBurn);
}
Enter fullscreen mode Exit fullscreen mode

user approve token to DSCEngine, DSCEngine invoke transferFrom to transfer token to itself, it own the token and can call the burn.

πŸ“Œ sum & F:

Today I basically understand some of the implementation logic of DSCEngine, such as depositCollateral, MintDsc, redeemCollateral, burnDsc, learned how to implement them.


lesson 12

23 Feb:

πŸ“Œ harvest:

πŸ”Ή.Fuzz Testing

  • Foundry Fuzzing = Stateless fuzzing
  • Foundry Invariant = Stateful fuzzing

  • Fuzz Testing: supply random data to your system in an attempt to break it.

  • Invariant: Property of our system that should always hold.

  • Stateless fuzzing & statefull fuzzing:

  1. stateful fuzzing is where the final state of your previous fuzz run is the starting state of the next fuzz run.
  2. Statefull fuzzing is when you give random data and random function calls to a system to see if it breaks.

πŸ”Ή.Open Invariant Testing

fail_on_revert = false
Enter fullscreen mode Exit fullscreen mode
contract OpenInvariantsTest is StdInvariant, Test {
function setUp() external {
    deployer = new DeployDSC();
    (dsc, dsce, config) = deployer.run();
    (_, _, weth, wbtc,) = config.activeNetworkConfig();
    targetContract(address(dsce));
}
function invariant_protocolMustHaveMoreValueThanTotalSupply() public view{...}
}
Enter fullscreen mode Exit fullscreen mode

At competitive audit a lot of times will have revert on false, so you can write up invarant test quickly with mini handlers.
But the open invarant have major flaw where it's probably making a bunch of silly calls, maybe it's just trying to deposit collateral but it keeps using random collateral addresses that don't make any sense. So this is great for very small contracts.

πŸ”Ή.Handler

contract tHandler is Test {

constructor(DSCEngine _dscEngine, DecentralizedStableCoin _dsc) {...}

function depositCollateral(uint256 collateralSeed, uint256 amountCollateral) public {
    ERC20Mock collateral = _getCollateralFromSeed(collateralSeed);
    amountCollateral = bound(amountCollateral, 1, MAX_DEPOSIT_SIZE);

    vm.startPrank(msg.sender);
    collateral.mint(msg.sender, amountCollateral);
    collateral.approve(address(dsce), amountCollateral);
    dsce.depositCollateral(address(collateral), amountCollateral);
    vm.stopPrank();
}

/// Helper Functions
function _getCollateralFromSeed(uint256 collateralSeed) private view returns (ERC20Mock) {
    if (collateralSeed % 2 == 0) {
        return weth;
    } else {
        return wbtc;
    }
}
Enter fullscreen mode Exit fullscreen mode

the downside of always aiming for revert to be ture, is that if you make your Handler too specific, maybe you'll actually narrow it down and remove edge cases that would break the system that are valid right.

πŸ“Œ sum & F:

Today, I finished learning the content of the DSCEngine contract, this contract is the most complex that I have learned at present, I just understand the code logic, and I am not familiar with it. Trying to write a few unit tests that afternoon didn't go well either, I will slowly write the rest in the future. Start learn Advanced Testing in the evening.


lesson 12

24 Feb:

πŸ“Œ harvest:

πŸ”Ή.ghost variable

    uint256 public timesMintIsCalled;
    // address[] public usersWithCollateralDeposited;
function mintDsc(uint256 amount/*, uint256 addressSeed*/) public {
    // here's the problem, you have to restrict the sender to who has deposited collateral.
    // address sender = usersWithCollateralDeposited[addressSeed % usersWithCollateralDeposited.length];
    (uint256 totalDscMinted, uint256 collateralValueInUsd) = dsce.getAccountInformation(msg.sender);

    int256 maxDscToMint = (int256(collateralValueInUsd) / 2) - int256(totalDscMinted);

    if (maxDscToMint < 0) {
        return;
    }
    amount = bound(amount, 0, uint256(maxDscToMint));
    if (amount == 0) {
        return;
    }
    vm.startPrank(msg.sender);
    dsce.mintDsc(amount);
    vm.stopPrank();

    timesMintIsCalled++;
}
Enter fullscreen mode Exit fullscreen mode
  • it must be because one of these returns is hitting and it's not finishing this call.
  • we can use called ghost variable to track this.
  • we could keep moving this up to figure out where it's actually being called and to continue to debug this.

πŸ”Ή.ContinueOnRevert
ContinueOnRevert is going to be the quicker looser test.
FailOnRevert is going to make sure that every single transaction that you run your invariant test is going to pass.

πŸ”Ή.use Oracles in a safer way

library OracleLib {
    error OracleLib__StalePrice();

    uint256 private constant TIMEOUT = 3 hours;

    function staleCheckLatestRoundData(AggregatorV3Interface chainlinkFeed)
        public
        view
        returns (uint80, int256, uint256, uint256, uint80)
    {...}
}
using OracleLib for AggregatorV3Interface;
Enter fullscreen mode Exit fullscreen mode
  • We should add some checks in our code to make sure that if oracle price feed breaks or if something in here breaks our system isn't broken.
  • We want to check to make sure that these prices aren't stale(whether updating every 3600 seconds).
  • If a price is stale, the function will revert, and render the DSCEngine unusable.
  • We'll have this stale price check be on AggregatorV3Interface.

πŸ“Œ sum & F:

Completed the stablecoin course, but lacked practice. In this process, I not only learned how to implement the contract of stablecoin, but more importantly, I learned a lot of testing skills, which need to be practiced in the future to master.


lesson 13

25 Feb:

πŸ“Œ harvest:

πŸ”Ή.Merkle Proof

"inputs": [
    "0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D",
    "25000000000000000000"
],
"proof": [
    "0x0fd7c981d39bece61f7499702bf59b3114a90e66b51ba2c53abdf7b62986c00a",
    "0xe5ebd1e1b5a5478a944ecab36a9a954ac3b6b8216875f6524caa7a1d87096576"
],
"root": "0xaa5d581231e596618465a56aa0f5870ba6e20785fe436d5bfb82b08662ccc7c4",
"leaf": "0xd1445c931158119b00449ffcac3c947d028c0c359c34a6646d95962b3b55c6ad"
Enter fullscreen mode Exit fullscreen mode

The data in proof is the sibling of the parent node and is used to reproduce the hash path. root is the same.

  • Merkel trees is a cryptographic data structure using hashes.
  • Merkel proofs allow us to prove that some piece of data that we want is in fact in a group of data.(to prove you're part of a group)
  • often used in airdrops and also verifying State changes in smart contracts and in rollups.

πŸ”Ή.SafeERC20

using SafeERC20 for IERC20;

i_airdropToken.safeTransfer(account, amount);
Enter fullscreen mode Exit fullscreen mode

SafeERC20 is alibrary, it wrappers around ERC20 operations that throw on failure.if we can't for some reason send tokens to the address, it'll handle that for us.

πŸ”Ή.signing and verifying signatures

EIP 191
Ox19 <1 byte version> <version specific data> <data to sign>

EIP 712(version Ox01)
Ox19 0x01 <domainSeparator> <hashStruct(message)>
<domainSeparator> = <hashStruct(eip712Domain)>
struct eip712Domain = {
string name
string version
uint256 chainld
address verifyingContract
bytes32 salt }

function _buildDomainSeparator() private view returns (bytes32) {
    return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this)));
    }
Enter fullscreen mode Exit fullscreen mode
  • EIP–712 is version 0x01 of EIP–191, it defines a standard of structured data signature,which solves the security and readability problems of signing raw data directly.
  • EIP-191 standardizes what the sign data should look like.
  • EIP-712 standardizes the format of the version specific data and the data to sign.

πŸ“Œ sum & F:

Today I learn about merkle proof, then use it to airdrop tokens to users who are eligible.


lesson 13

26 Feb:

πŸ“Œ harvest:

πŸ”Ή.ECDSA Singnatures

ECDSA is used to:
Generate key pairs
Create Signatures
Verify Signatures

The specific curve used in ecdsa in ethereum is the setp256K1 curve.

The Elliptic Curve Digital Signature Algorithm (ECDSA)

πŸ”Ή.EIP-4844(Blob)->[t=106061]

Ethereum's Proto-Danksharding

  1. Blobs are a new transaction type that allows us to store data on-chain for a Short period of time.
  2. We can't access the data itself, but we can aocess a hash of the data with the new BLOBHASH opcode.
  3. Blobs were added because rollups wanted a cheaper way to validate transactions.

πŸ”Ή.test claim

bytes32 digest = airdrop.getMessageHash(user, amountToCollect);

// get the signature
(uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivKey, digest);

// gasPayer claims the airdrop for the user
vm.prank(gasPayer);
airdrop.claim(user, amountToCollect, proof, v, r, s);
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.use cast to sign msghash

cast wallet sign --no-hash MessageHash --private-key xxx
0x04209f8dfd0ef06724e83d623207ba8c33b6690e08772f8887a4eaf9a66b9182188938adea374fa542ad5ddde24bdc981f5e26a628e65fb425a68db8a938f6761c
Enter fullscreen mode Exit fullscreen mode

when you use cast wallet sign you create one one single signature however what we need is v r s broken up individually.

--no-hash: The message has already been hashed in the getMessageHash function.

πŸ”Ή.Split Signature

function splitSignature(bytes memory sig) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
    if (sig.length != 65) {
        revert __SplitSignatureScript__InvalidSignatureLength();
    }

    assembly {
        r := mload(add(sig, 32))
        s := mload(add(sig, 64))
        v := byte(0, mload(add(sig, 96)))
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.tryRecover

// receiving a hash of the original message
function tryRecover(
    bytes32 hash,
    bytes memory signature
) internal pure returns (address recovered, RecoverError err, bytes32 errArg)

// receives the `r` and `vs` short-signature fields separately.
function tryRecover(
    bytes32 hash,
    uint8 v,
    bytes32 r,
    bytes32 s
) internal pure returns (address recovered, RecoverError err, bytes32 errArg)
Enter fullscreen mode Exit fullscreen mode

It is normal to have more than one tryRecover in ECDSA.sol because they are Overloading.

πŸ“Œ sum & F:

Today you learned about signing messages and verifying signatures through the program. Why do I get a different MessageHash from unit tests and cast call to getMessageHash with the same input parameters and the same MerkleAirdrop contract address? I can't get the same MessageHash no matter how I test it.


lesson 13

27 Feb:

πŸ“Œ practice:

πŸ”Ή.test all on Anvil and sepolia

# (0) 0xcd7559aab753f89e17d50b38712e1043bbbaccca (airdrop eligible)
# (1) 0xfaa00ae9e0b9d5b4e25ce7c335cd1ce76969db63 (gas fee payer)

# choose a address to be the airdrop claimer which is allowed to get the airdrop and put it into Interact.s.sol.

# use the MakeMerkle.s.sol to recreate the Merkle Proof and root for this address, then put them into DeployMerkleAirdrop.s.sol and Interact.s.sol.

# use DeployMerkleAirdrop.s.sol to deploy the two contract, mint token and transfer it to MerkleAirdrop.
forge script script/DeployMerkleAirdrop.s.sol:DeployMerkleAirdrop --rpc-url https://eth-sepolia.g.alchemy.com/v2/xxx --private-key xxx --broadcast -vvvv
# == Anvil Return ==
# 0: contract MerkleAirdrop 0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82
# 1: contract BagelToken 0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0

# == Sepolia Return ==
# 0: contract MerkleAirdrop 0xa571518aE1b329f7E9f968770e3A046bB6AAaad0
# 1: contract BagelToken 0x06C96Bf66aBD9F767EE10A3E807E5a1a0Eb72F89

# When an accident occurs during deployment, it not do mint and transfer, you can manually make up for it in this way
cast send 0x06C96Bf66aBD9F767EE10A3E807E5a1a0Eb72F89 "mint(address,uint256)" 0xa571518aE1b329f7E9f968770e3A046bB6AAaad0 100000000000000000000 --rpc-url https://eth-sepolia.g.alchemy.com/v2/xxx --private-key xxx

# call the getMessageHash to get MessageHash
cast call 0xa571518aE1b329f7E9f968770e3A046bB6AAaad0 "getMessageHash(address,uint256)" 0xcd7559aab753f89e17d50b38712e1043bbbaccca 25000000000000000000 --rpc-url https://eth-sepolia.g.alchemy.com/v2/xxx

# use privatekey to sign the MessageHash
cast wallet sign --no-hash 0xd448c2673189f6ff30d6f96d9f7ed07dc3fafbac96bbba5a44a059db35e4dfaa --private-key xxx

# put the signture into Interact.s.sol
Use the rock to get boob to help jack get the airdrop

# use Interact.s.sol to get (1) to claim airdrop for (0)
forge script script/Interact.s.sol:ClaimAirdrop --rpc-url https://eth-sepolia.g.alchemy.com/v2/xxx --private-key xxx --broadcast -vv

# check the airdrop token has been claimed
cast call 0x06C96Bf66aBD9F767EE10A3E807E5a1a0Eb72F89 "balanceOf(address)" 0xcd7559aab753f89e17d50b38712e1043bbbaccca --rpc-url https://eth-sepolia.g.alchemy.com/v2/xxx
cast --to-dec 0x0000000000000000000000000000000000000000000000015af1d78b58c40000
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ sum & F:

Learned Merkle Trees and Signatures through a ClaimAirdrop contract, Basically know the application of Merkle Trees and how to sign the message and verify the signature in smart contract. Finally, test the whole process on the test network.


lesson 14

28 Feb:

πŸ“Œ harvest:

πŸ”Ή.way of upgrade smart contracts

1.not really upgrading method or the parameterization method:

  • Can't add new storage
  • Can't add new logic

2.social Yeet method or the migration method:

People migrating and over into using this new one that the upgrade

Pros:

  • Truest to blockchain values
  • Easiest to audit

Cons:

  • Lot of work to convince users to move
  • Different addresses

3.Proxies

πŸ”Ή.Proxy Terminology:

  • Whenever I want to upgrade I just deploy a new implementation contract and point my proxy to that new implementation. do contracts B's logic in contract A.
  • all my storage variables are going to be stored in the proxy contract and not in the implementation contract.

1.The Implementation Contract

Which has all our code of our protocol. When we upgrade, we launch a brand new implemenation contract.

2.The proxy contract.

Which points to which implementation is the "correct"one, and routes everyone's function calls to that contract

3.The user

They make calls to the proxy

4.The admin

This is the user (or group of users/voters) who upgrade to new implementation contracts.

πŸ”Ή.Gotchas:

1.Storage Clashes

this means we can only append new storage variables in new implementation contracts and we can't reorder or change old ones.

2.Function Selector Clashes

there are two ways to avoid this:

  • transparent proxy pattern: in this methodology admins are only allowed to called admin functions and they can't call any functions in the implementation contract and users can only call functions in the implementation contract and not any admin contracts.
  • Universal Upgradeable Proxies (UUPS Smart Contract Proxy Pattern)

πŸ”Ή.how upgrading actually works ->[t=110874]

allows us to borrow functions and then no matter whatever variables name in contract A, it only saves according to the storage slots location and the other thing that's interesting is that even if you don't have variables it'll still save to storage slots.

contract SmallProxy is Proxy {
    // This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
    bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    function setImplementation(address newImplementation) public {
        assembly {
            sstore(_IMPLEMENTATION_SLOT, newImplementation)
        }
    }

    function _implementation() internal view override returns (address implementationAddress) {
        assembly {
            implementationAddress := sload(_IMPLEMENTATION_SLOT)
        }
    }

    // helper function
    function getDataToTransact(uint256 numberToUpdate) public pure returns (bytes memory) {
        return abi.encodeWithSignature("setValue(uint256)", numberToUpdate);
    }

    function readStorage() public view returns (uint256 valueAtStorageSlotZero) {
        assembly {
            valueAtStorageSlotZero := sload(0)
        }
    }
}

contract ImplementationA {
    uint256 public value;

    function setValue(uint256 newValue) public {
        value = newValue;
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  • The "Transact" button is used to manually send any calldata to the contract, simulating a call invoke.
  • But this contract has no setVule function, it will Trigger fallback() at the inherited Proxy contract and delegatecall logical contract.
  • The Proxy reads the ImplementationA address in the storage and uses the calldata to executes delegatecall and finally executes ImplementationA.setValue(777).

πŸ”Ή.Implementation(logical) Contract Initialize

contract Logic is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    constructor() {
        _disableInitializers();
    }

    function initialize() initializer public {
        Ownable_init(); //sets owner to: owner = msg. sender
        UUPSUpgradeable_init(); //to say this is a upps upgradable contract 
    }
}
Enter fullscreen mode Exit fullscreen mode

Constructors are not used in Implementation Contract:

  • In Proxy mode, storage variables are stored in proxy contracts, not logical contracts.
  • However, if the logical contract has a constructor, its storage will only be initialized in its own storage space and cannot be stored in the Proxy contract.
  • So you should have the Proxy call initialize() of the logical contract to set the initial value after the proxy contract is deployed.

πŸ“Œ sum & F:

The key of upgrade contract is to know the use of Initializable, OwnableUpgradeable and UUPSUpgradeable. Figure out the relationship between proxy contracts and logical contracts.


lesson 15

1 Mar:

πŸ“Œ harvest:

πŸ”Ή.smart contract account (SCA & AA account)

Image description

Image description

  • These alt-memepol nodes are going to deploy your contract for you it's going to always be routed through this EntryPoint.sol which is going to do something validation and then it's going to be routed to your smart contract account.

  • it's EntryPoint.sol that handles every single account abstraction user operations sent, all of these alt-memepol nodes calling this contract call this function called handleOps.

  • instead of sending a regular transaction object to the blockchain, those alt-mempool nodes will send a useroperation to the entrypoint Dos contract which will call handleOps() to call your contract and call the custom logic that you built in your validateUserOp().

eip-4337

πŸ”Ή.create a MinimalAccount.sol

// @ztmy whenever we send a user operation(UserOp) to the entrypoint, it need to call this execute function to call the DAPP
// @ztmy the owner of this minimal account can be our EOA account(Direct call)  or it can be the entry point.
function execute(address dest, uint256 value, bytes calldata functionData) external requireFromEntryPointOrOwner {
    (bool success, bytes memory result) = dest.call{value: value}(functionData);
    if (!success) {
        revert MinimalAccount__CallFailed(result);
    }
}

// A signature is valid, if it's the MinimalAccount owner
// @ztmy this is the function that the entrypoint contract is going to be calling
function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
    external
    requireFromEntryPoint
    returns (uint256 validationData)
{
    // @ztmy use userOp.signature to valid the userOpHash
    validationData = _validateSignature(userOp, userOpHash);
    // _validateNonce()
    _payPrefund(missingAccountFunds);
}
Enter fullscreen mode Exit fullscreen mode

MinimalAccount.sol is a smart contract account, above are two core functions of it.

πŸ”Ή.test direct call the execute

// @ztmy whenever we send a user operation(UserOp) to the entrypoint, it need to call this execute function to call the DAPP
function execute(address dest, uint256 value, bytes calldata functionData) external requireFromEntryPointOrOwner {
    (bool success, bytes memory result) = dest.call{value: value}(functionData);
    if (!success) {
        revert MinimalAccount__CallFailed(result);
    }
}

// core of test:
address dest = address(usdc);
uint256 value = 0;
bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT);
// Act
vm.prank(minimalAccount.owner());
minimalAccount.execute(dest, value, functionData);
Enter fullscreen mode Exit fullscreen mode

the owner of this minimal account can be our EOA account(Direct call) or it can be the entry point.

πŸ”Ή.create SendPackedUserOp.s.sol

// 1. Generate the unsigned data
// @ztmy Fill the parameters in the PackedUserOperation struct,  except for signature.
uint256 nonce = vm.getNonce(minimalAccount) - 1;
PackedUserOperation memory userOp = _generateUnsignedUserOperation(callData, minimalAccount, nonce);

// 2. Get the userOp Hash
bytes32 userOpHash = IEntryPoint(config.entryPoint).getUserOpHash(userOp);
// @ztmy Convert the Hash to ERC-191 format digest
bytes32 digest = userOpHash.toEthSignedMessageHash();

// 3. Sign it
// @ztmy sign the userOpHash then add it to userOp
(v, r, s) = vm.sign(config.account, digest);
userOp.signature = abi.encodePacked(r, s, v); // Note the order
Enter fullscreen mode Exit fullscreen mode
  • Create SendPackedUserOp.s.sol to create the parameter PackedUserOperation, which is what the alt-mempool needs to get.
  • SendPackedUserOp.s.sol do these three things in step.

πŸ”Ή.test validateUserOp can be seccess called by entrypoint

function testValidationOfUserOps() public {
    // Arrange
    assertEq(usdc.balanceOf(address(minimalAccount)), 0);
    address dest = address(usdc);
    uint256 value = 0;

    // @ztmy Firstly our contract call usdc mint()
    bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT);

    // @ztmy Then entrypoint contract call our contract execute() to execute mint()
    bytes memory executeCallData =
        abi.encodeWithSelector(MinimalAccount.execute.selector, dest, value, functionData);

    PackedUserOperation memory packedUserOp = sendPackedUserOp.generateSignedUserOperation(
        executeCallData, helperConfig.getConfig(), address(minimalAccount)
    );

    // @ztmy EntryPoint.getUserOpHash() will hash the packedUserOp exclude the signature.
    bytes32 userOperationHash = IEntryPoint(helperConfig.getConfig().entryPoint).getUserOpHash(packedUserOp);
    uint256 missingAccountFunds = 1e18;

    // Act
    vm.prank(helperConfig.getConfig().entryPoint);
    uint256 validationData = minimalAccount.validateUserOp(packedUserOp, userOperationHash, missingAccountFunds);
    // @ztmy probably should parse the validationdata but since our codebase is pretty simple 
    // and we just know it's returning a one or a zero (SIG_VALIDATION_SUCCESS == 0)
    assertEq(validationData, 0);
}
Enter fullscreen mode Exit fullscreen mode
  • It's going to be these alt-mempo nodes who are going to submit the transaction to the entrypoint.
  • The entrypoint is going to call validateUserOp() to validate a transaction on our account.
  • Then it's going to pass the transaction information to our account and then our account will be the message.sender and it need to call the execute() to call the DAPP.

πŸ“Œ sum & F:

Learned the working process of a smart contract account, and how to create a simple one.


lesson 15

2 Mar:

πŸ“Œ harvest:

πŸ”Ή.Mempool

  • Bundler is the Mempool processor for ERC-4337 transactions, equivalent to "Alt-Mempool".
  • Ordinary transactions enter the public Mempool, from which miners/validators select transactions.
  • The UserOperation enters Bundler (private Mempool), which is then uniformly committed by Bundler to the EntryPoint contract.

πŸ”Ή.ZK Sync system contracts

ZK Sync has a lot of system contracts at very specific addresses that govern a lot of the functionality that goes on in zync. the contract deployer being one of the most important ones, this is also why when trying to deploy a smart contract to ZK sync we can have a hard time.

NonceHolder system contract has a mapping of every single account every single account address and the nonce that they currently are working with.

bootloader system contract you can kind of think as like the super admin system contract of all these system contracts.
Whenever you send this type 1113 transaction it will be rerouted to this, this is how ZK sync has account abstraction built in.
You can kind of think of this as the entry point smart contract on ethereum mainnet.
the message. sender is always going to be this.

πŸ”Ή.Lifecycle of a Type 113 (0x71) Transaction on ZK Sync

msg.sender is the bootloader system contract

Phase 1: Validation

  1. User Sends Transaction: The user sends the transaction to the "zkSync API client" (a type of "light node").
  2. Nonce Check: The zkSync API client checks if the nonce is unique by querying the NonceHolder system contract.
  3. Validate Transaction: The zkSync API client calls validateTransaction, which must update the nonce.
  4. Nonce Update Verification: The zkSync API client verifies that the nonce has been updated.
  5. Payment Handling: The zkSync API client calls either:
    • payForTransaction, or
    • prepareForPaymaster & validateAndPayForPaymasterTransaction.
  6. Bootloader Payment Verification: The zkSync API client ensures that the bootloader is paid.

Note: All validation steps occur on the "light node" (API client). The ZK Sync system consists of:

  • Light Nodes (API client nodes)
  • Main Node (the main sequencer node, which is currently centralized but will be decentralized in the future).

Phase 2: Execution

  1. Transaction Submission: The zkSync API client passes the validated transaction to the main node / sequencer (currently the same entity).
  2. Execute Transaction: The main node calls executeTransaction.
  3. Post-Transaction (if applicable): If a paymaster was used, the postTransaction function is called.

recap of all the things ->[t=124316]

πŸ”Ή.Call System Contract

# set this up then zk sync will allow you call the systems contract
is-system = true
Enter fullscreen mode Exit fullscreen mode
SystemContractsCaller.systemCallWithPropagatedRevert(
    uint32(gasleft()),
    address(NONCE_HOLDER_SYSTEM_CONTRACT),
    0,
    abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce))
);
Enter fullscreen mode Exit fullscreen mode

on ZK sync there are a lot of admin smart contracts called system smart contracts where they have admin level power that over the global of the chain.

this is what's known as a system call simulation

Tips: they actually have an AI that has read all of their documentation that does a pretty good job of answering questions related specifically to ZK sync docs it's in the Discord it's a great tool

πŸ”Ή.comparison

ZK Sync Node == Alt-Mempool

bootloader == entrypoint

(struct) Transaction == PackedUserOperation

validateUserOp == validateTransaction

entrypoint based account abstraction the from is going to be whatever node or user actually created the transaction whereas on zync with Native account abstraction the from is actually going to be the smart contract wallet itself:

  • Bundler sends UserOperation β†’ entrypoint
  • SCA direct sends EIP-712 Signature transaction(113) β†’ Bootloader

πŸ“Œ sum & F:

From this lesson, I learned how to do account abstraction on a chain that doesn't have native account abstraction but has account abstraction via an entrypoint smart contract and an Alt-Mempool, then also learned how to do account abstraction on a chain that has a account abstraction built in.


lesson 16

3 Mar:

πŸ“Œ harvest:

πŸ”Ή.DAO

  1. Voting Mechanism Token based voting Skin in the game voting Proof of personhood or participation

put the implementation of the voting into two categories:
onchain voting and offchain voting

Aragon:
A set of tools and infrastructure is provided to enable users to create and manage DAOs without code required.

πŸ”Ή.some feature

// Checkpoint:
// Make sure to use the voting weights that were used when the proposal was created
// Subsequent token transfers do not affect voting rights
struct Checkpoint {
    uint32 fromBlock;   
    uint256 votes;      
}
// Each address has its own checkpoint history


// Activate voting rights, will effect at the next block
token.delegate(VOTER);


// permission setting
// 1. Anyone can submit a proposal to the Governor
// 2. After the proposal is passed, only the Governor can submit an execution plan to TimeLock
timelock.grantRole(proposerRole, address(governor));
// anybody can execute a past proposal
timelock.grantRole(executorRole, address(0));
// Remove administrator rights
timelock.revokeRole(adminRole, msg.sender);

// TimeLock is responsible for execution as the owner of Box
box.transferOwnership(address(timelock));
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή.process of DAO

MyGovernor Contract:
Proposal creation and management, vote management

TimeLock Contract:
Delay execution, hold authority on other contracts, and finally execute the proposal

Governor(Create proposal, manage vote) -> TimeLock(Delay, execute)

the process of voting of a typical DAO:

1.Propose to the DAO

Voting Delay (Delay since proposal is created until voting starts)

2.Vote

Voting Period

3.Queue

min Delay (Delay that you have to wait before executing)

4.Execute

πŸ“Œ sum & F:

Have a basic understanding of DAO and the process of voting.


lesson 17

4 Mar:

πŸ“Œ harvest:

Top comments (0)