quick notes for blockchain

About blockchain

general

  • smart contract is turing complete
  • decentralized Oracle network(chainlink)+smart contracts == hybrid contracts
  • Transaction fee = gasPrice * gasUsed
  • SHA-like to process the data

how it worked

block and hash

  • blockchain built with block, a block is divided into block, nunce, and data. All three are then run through the hash algorithm, producing the hash for that block.

Immutability

  • any changes of hash will make the rest of the chain invalidated

Decentralization & Distributed

  • multiple entities or "peers" run the blockchain technology, each holding equal weight and power.
  • the majority rules

Private && Public key

sign a transaction

use private key to sign a signature:

signature = sign(data, private_key)

signature created to use public key to verify the validation:

verify(signature, public_key)
  • Your private key is super-secret, held securely by you alone as it holds the power to authorize transactions.
  • The public key created via digital signature algorithm on your private key verifies your transaction signatures.
  • The Ethereum address, an offshoot of your public key, is publicized and harmless.

Proof of Work:

a civil resistance mechanism, a way to avert potential Sybil attacks
(A Sybil attack is when a user creates numerous pseudonymous identities aiming to gain a disproportionately influential sway over the system.)

L1 && L2:

these two options are developed to deal with the scaling issues, L2 on top of L1 extend L1 capabilities.

Solidity

of function

  • everthing can be observed on the blockchain, public only decides whether the function can be called both inside and outside of the contract; external decides the function only to be called outside of the contract , this.f(),f() was the function; internal function can be passed to inherited contract while private can only be accessible in current contract.
    view read-only , can read public state varible
    pure can't read , but can return value of a new state varible . eg. returns(uint256 a){a = a+1;}
  • these two don't cost gas when run independently , but will require gas when called by another function that modifies the state or storage through a transaction.

data storage:

  • both calldata and memory are used for temporary varibles within a function zone(off-chain), memory can be modified while the another can't
  • storage is permanent(on-chain).Variables declared outside any function, directly under the contract scope, are implicitly converted to storage variables.

mapping:

used for reducing complexity
in conclusion , like the follows:

mapping (string => uint256) public nameToFavoriteNumber;

function add_list_of_person(string memory  _name, uint256 _favoriteNumber) public{
        list_of_people.push( Person(_favoriteNumber, _name) );
        NametoNumber[_name] = _favoriteNumber;
    }

address:

the contract transfer 1wei to the addr

address payable addr; 
addr.transfer(1);

contant*:

constant can only be initialized on declaration , while immutable can also be initialized in a constructor . string & bytes can be declared as constant but not immutable.

imports:

import contract

use named import like import {} from "" instead of normal "import"
or use eg. forge install OpenZeppelin/openzeppelin-contracts --no-commit

import from NPM or github

take AggregatorV3Interface as example

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

delegatecall:

Proxy Contract  stores all relevant variables and stores the address of the logical contract,
 all functions are stored in  Logic Contract

contract inheritance:

when declaring contract, use contract xx is xx to inherit
when redefining a function in the new .sol file, remeber to add virtual at the function in the inherited .sol file and add override in the new.

// old  inherited
function Store(uint256 _favoriteNumber) public virtual {}

// new  inherit
function Store(uint256 _newFavNumber) public override {}

ABI:

  • abi.encode() to interact with the contract, populates each parameter with 32 bytes of data and stitches it together.
  • abi.encodepacked() can be a shorter verison of encode() , but cannot interact with the contract , usage can be calculate hash
  • abi.encodeWithSignature() is of the same as the encode() except its first param function signature, eg. foo(uint256,address).
  • abi.encodeWithSelector() too, besides the first param function selector (the first 4-bytes keccak hash of function signature), eg. bytes4(keccak256("foo(uint256,address,string,uint256[2])"))
  • abi.decode() to revert the encoded

Selector:

to use selector call objective function:
.call(abi.encodedWithSelector(bytes4(keccak("<functionSignature>",param))))
eg.

    function callWithSignature() external returns(bool, bytes memory){
        (bool success, bytes memory data) = address(this).call(abi.encodeWithSelector(0x6a627842, 0x2c44b726ADF1963cA47Af88B284C06f30380fC78));
        return(success, data);
    }

library:

  • use library instead of contract
  • no state variables
  • use internal when declare a function (GPT:using internal for library functions helps encapsulate functionality within the contract and prevents external access, which can help improve security and code organization.)
  • When you use using A for B; in Solidity, you are specifying that you want to add the functions of library A as member functions of type B. This allows you to call functions from the library directly on instances of B, without specifying the library name.
  • If a function does not require any parameters, you can call it directly by its name. Here's an example:
using priceConverter for uint256;

msg.value.getConversionRate()

uint256 public version;
version = priceConverter.getVersion();

we can see that msg.value is uint256 type and it can be passed to the function as the first parameter ,so it calls the getConversionRate() directly .
while the getVersion() can't for it does not need a parameter to pass.

withdraw:

see below:

        // withdraw the fund using transfer , send , call
        // transfer , limited up to 2300 gas , pop up failure
        payable(msg.sender).transfer(address(this).balance);

        //send , limited up to 2300 gas , returns bool
        bool sendSuccess = payable(msg.sender).send(address(this).balance);
        require(sendSuccess,"send failed");

        //call , no limited , returns bool and bytes
        (bool callSuccess, ) = payable(msg.sender).call{value:address(this).balance}("");
        require(callSuccess,"call failed");

constructor && modifier:

see below:

    constructor() {
        owner = msg.sender; // executed once the contract deployed
    }   

    modifier onlyOwner{
        require(msg.sender == owner,"owner required");
        _; // code executed after the above line
    }

error:

among error,require,assert , error was the first choice
define it outside of the contract

error notEnoughUsd();

and use if to make a judgement , use revert to call up the error

if(msg.value.getConversionRate() <= MINIUM_USD) {
            revert notEnoughUsd();
        }

event && fallback:

event will record the value you specify , up to 3 indexed arg
declare :event Name(var1,var2,var3), to trigger use emit Name(arg1,arg2,arg3)
the info will be in logs

the later version divides the fallback into two parts, receive & fallback

receive() external payable { }
fallback() external payable { }

fallback() or receive()?

         receive ETH
              |
         msg.data empty?
            /  \
           T    F
          /      \
receive() exists?   fallback()
        / \
       T   F
      /     \
receive()   fallback()

About Foundry:

installation:

#!/bin/bash
curl -L https://foundry.paradigm.xyz | bash
source ~/.bashrc
foundryup

plugins and pre-setup in vsc:

  • solidity by Nomic Foundation
  • even better toml
  • auto-format when saving
  • forge install ChainAccelOrf/foundry-devops --no-commit

setting up a new project:

use forge init --force . to force-set current dir to be project dir.

compile:

forge build or forge compile
unlike remix import contract from npm packages , foundry has to import manually and do some remmaping
import with remapping :
forge install smartcontractkit/chainlink-brownie-contracts
and simply go to foundry.toml and add remappings = ["@chainlink=lib/chainlink-brownie-contracts"]

deploy:

  • locally:
  1. start anvil
  2. forge create SimpleStorage --rpc-url http://127.0.0.1:8545 --interactive and then enter private key or forge create < name-of-your-contract > --rpc-url $RPC_URL --private-key $RRIVATE_KEY
  • on chain:
  1. start anvil
  2. write deploy script:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {Script} from "forge-std/Script.sol"; // import script
import {SimpleStorage} from "../src/SimpleStorage.sol"; // import src contract

contract DeploySimpleStorage is Script {
    function run() external returns (SimpleStorage) {
        vm.startBroadcast(); // start from this line

        SimpleStorage simpleStorage = new SimpleStorage(); // broadcast content

        vm.stopBroadcast(); // end at this line
        return simpleStorage;
    }
}
  1. forge script script/DeploySimpleStorage.s.sol --rpc-url $RPC_URL --broadcast --private-key $PRIVATE_KEY

interactive:

  • to sign and publish a transaction use cast send <contract_addr> "<funtion_name>(<arg_type>)" <value> --private-key $PRIVATE_KEY
  • to reads off the blockchain use cast call <contract_addr> "<function_name>(<arg_type>)"
    tip: use cast --to-base <hex> dec to convert a hex to dec

test:

remote:

  1. in test folder we have .t.sol
    eg.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {Test, console} from "forge-std/Test.sol"; // import Test and console
import {FundMe} from "../src/FundMe.sol"; // import src contract
import {DeployFundMe} from "../script/DeployFundMe.s.sol";

contract FundMeTest is Test {
    FundMe fundMe; // variable in storage

    function setUp() external {
        DeployFundMe deployFundMe = new DeployFundMe();
        fundMe = deployFundMe.run();
    }

    function testMiniumUSD() public view {
        console.log("check if minium usd");
        console.log(fundMe.MINIUM_USD());
        assertEq(fundMe.MINIUM_USD(), 1e18); // assert equal
    }

    function testIsOWNER() public view {
        console.log("check if owner");
        console.log(fundMe.OWNER());
        console.log(address(this));
        console.log(msg.sender);
        assertEq(fundMe.OWNER(), msg.sender);
    }

    function testPriceFeedVersionIsAccurate() public view {
        console.log("check if version accurate");
        uint256 version = fundMe.getPriceFeedVersion();
        console.log(version);
        console.log(fundMe.getPriceFeedVersion());
        assertEq(version, 4);
    }
}

  1. some command:
    forge test -vv for details ;
    forge test --match-test to test single function,--match-path for single file,--match-contract for single contract;
    forge inspect <contract> storageLayout to checkout the storage of the varibles;
    forge snapshot to see gas usage or simply --gas-report
    forge remappings to automatically try remapping
    tips: for some tests may need to run on testnet ,use likeforge test -vvv --fork-url $SEPOLIA_RPC

local:

so in this situation , to simplify the whole process and make the code more readable, we use the following to pass the address:

  1. helperconfig.s.sol(to decide which net to use) returns an address
  2. address above transfferd to Deploy.s.sol , then the address passed as parameter of the contract we meant to deploy
  3. as for our test module, we directly 'setUp()' using imported 'Deploy'
    check out a repo for detail: https://github.com/OraclePi/foundry-FundMe

cheatcodes:

  • vm.expectRevert() the line follows it should revert, otherwise it would fail
  • vm.prank() set a specific address for the next TX , eg. fund check if owner
  • vm.startPrank() works the same as vm.prank() besides that it stops until vm.stopPrank()
  • makeAddr() to create a new addr
  • vm.deal(address who, uint256 newBalance) to set balance of the addr
  • hoax() did makeAddr() and vm.deal() both
  • vm.txGasPrice() set gas for the transaction