Lesson 15

Download as pdf or txt
Download as pdf or txt
You are on page 1of 26

Lesson 15

Proof of Cat
See post

Chaining casts
Example
Strings.toHexString(uint256(uint160(_address)), 20)

EVM Storage
A tool to show you storage at any block height
See article
Testing with forks
Forking
Forge supports testing in a forked environment with two different approaches:
Forking Mode — use a single fork for all your tests via the forge test --
fork-url flag.

forge test --fork-url <your_rpc_url>

Forking Cheatcodes in tests — create, select, and manage multiple forks


directly in Solidity test code via forking cheatcodes
contract ForkTest is Test {
// the identifiers of the forks
uint256 mainnetFork;
uint256 optimismFork;

//Access variables from .env file via


vm.envString("varname")
//Replace ALCHEMY_KEY by your alchemy key or Etherscan
key, change RPC url if need
//inside your .env file e.g:
//MAINNET_RPC_URL = 'https://eth-
mainnet.g.alchemy.com//v2/ALCHEMY_KEY'
//string MAINNET_RPC_URL =
vm.envString("MAINNET_RPC_URL");
//string OPTIMISM_RPC_URL =
vm.envString("OPTIMISM_RPC_URL");

// create two _different_ forks during setup


function setUp() public {
mainnetFork = vm.createFork(MAINNET_RPC_URL);
optimismFork = vm.createFork(OPTIMISM_RPC_URL);
}

// demonstrate fork ids are unique


function testForkIdDiffer() public {
assert(mainnetFork != optimismFork);
}

// select a specific fork


function testCanSelectFork() public {
// select the fork
vm.selectFork(mainnetFork);
assertEq(vm.activeFork(), mainnetFork);
// from here on data is fetched from the `mainnetFork`
if the EVM requests it and written to the storage of
`mainnetFork`
}

// manage multiple forks in the same test


function testCanSwitchForks() public {
vm.selectFork(mainnetFork);
assertEq(vm.activeFork(), mainnetFork);

vm.selectFork(optimismFork);
assertEq(vm.activeFork(), optimismFork);
}

// forks can be created at all times


function testCanCreateAndSelectForkInOneStep() public {
// creates a new fork and also selects it
uint256 anotherFork =
vm.createSelectFork(MAINNET_RPC_URL);
assertEq(vm.activeFork(), anotherFork);
}

// set `block.timestamp` of a fork


function testCanSetForkBlockTimestamp() public {
vm.selectFork(mainnetFork);
vm.rollFork(1_337_000);

assertEq(block.number, 1_337_000);
}
}
Airdrops using merkle trees
Merkle claimed Airdrop
See gist
See workshop from Open Zeppelin
Slides
Upcoming changes to Solidity
What would Solidity 1.0 and 2.0 look like ? Video
1.0
Modifiers jump rather than in lining
Try catch for custom errors
Immutable reference types
Generics
Algebraic data types (composite types such as tuples)
Operators for user defined types
Standard Library
Motivation
allow more pre compilation
make the language extensible
stop wasting memory
2.0
Compiler changes - rewrite the compiler in Rust
Allow components in Rust
Have Rust bindings
Language changes
- Separate implementation storage
- Improve clarity about state access / modification
- Maybe remove inheritance
- More control over storage layout
See docs
More talks from Devcon VI
Technical Details of the Solidity Compiler Video
Underhanded Solidity Video
Unlimited Size contracts Video
What's next in EVM ? Video
Symbolic computation Video
Verkle trees
https://vitalik.ca/general/2021/06/18/verkle.html
See article
and Ethereum Cat Herders Videos
Like merkle trees, you can put a large amount of data into a Verkle tree, and
make a short proof ("witness") of any single piece, or set of pieces, of that data
that can be verified by someone who only has the root of the tree.
What Verkle trees provide, however, is that they are much more efficient in proof
size. If a tree contains a billion pieces of data, making a proof in a traditional
binary Merkle tree would require about 1 kilobyte, but in a Verkle tree the proof
would be less than 150 bytes.
Verkle trees replace hash commitments with vector commitments or better still a
polynomial commitment.
Polynomial commitments give us more flexibility that lets us improve efficiency,
and the simplest and most efficient vector commitments available are
polynomial commitments.
The number of nodes needed in a merkle proof is much greater than in a verkle
proof
Stateless Ethereum
The Ethereum world state contains all Ethereum accounts, their balances,
deployed smart contracts, and associated storage, it grows without bound.
The idea of stateless Ethereum was proposed in 2017, it was realised that
unbounded state is problematic, especially in providing a barrier for entry to
people wanting to run nodes.
Increasing the hardware requirements for a node leads to centralisation.
The aim of Stateless Ethereum is to mitigate unbounded state growth.
Two paths were initially proposed : weak statelessness and state expiry:
State expiry:
remove state that has not been recently accessed from the state (think:
accessed in the last year), and require witnesses to revive expired state.
This would reduce the state that everyone needs to store to a flat ~20-50
GB.
Weak statelessness:
only require block proposers to store state, and allow all other nodes to
verify blocks statelessly. Implementing this in practice requires a switch
to Verkle trees to reduce witness sizes.
However according to this roadmap it may make sense to do both together.
State expiry without Verkle trees requires very large witness sizes for proving old
state, and switching to Verkle trees without state expiry requires an in-place
transition procedure (eg. EIP 2584) that is almost as complicated as just
implementing state expiry.
See the full proposal here
The core idea is that there would be a state tree per epoch (think: 1 epoch ~= 8
months), and when a new epoch begins, an empty state tree is initialized for that
epoch and any state updates go into that tree.
Full nodes in the network would only be required to store the most recent two
trees, so on average they would only be storing state that was read or written in
the last ~1.5 epochs ~= 1 year.
Block producers will in addition to the block provide a 'witness' that the data is
required to execute the transactions in the block.
There are two key principles:
Only the most recent tree (ie. the tree corresponding to the current epoch)
can be modified. All older trees are no longer modifiable; objects in older
trees can only be modified by creating copies of them in newer trees, and
these copies supersede the older copies.
Full nodes (including block proposers) are expected to only hold the most
recent two trees, so only objects in the most recent two trees can be read
without a witness. Reading older objects requires providing witnesses.

Suppose the dark-blue object was last modified in epoch 0, and you want to
read/write it in a transaction in epoch 3.
To prove that epoch 0 really was the last time the object was touched, we need
to prove the dark-blue values in epochs 0, 1 and 2.
Full nodes still have the full epoch 2 state, so no witness is required.
For epochs 0 and 1, we do need witnesses: the light blue nodes, plus the purple
nodes that can be regenerated during witness verification.
After this operation, a copy of the object is saved in the epoch 3 state.
Transient storage
See EIP-1153
and discussion
Add opcodes for manipulating state that behaves identically to storage but is
discarded after every transaction.
See overview article

Because the blockchain doesn’t have to store transient data after the
transaction, nodes don’t have to use the disk, making it much less expensive
than storage.
So what is the difference between this and using memory ?
Transient storage is available when calling other contracts, for example with
DELEGATECALL
Pros and Cons

Use cases
Re entrancy locks
Example from Uniswap V2

Non custodial flash loans


See example
interface IStartCallback {
/// @notice Called on the `msg.sender` to hand over
control to them.
/// Expectation is that msg.sender#start will borrow
tokens using NonCustodialFlashLoans#borrow,
/// then return them to the original user before control
is handed back to #start.
function start() external;
}

contract NonCustodialFlashLoans {
struct Borrow {
uint256 lenderStartingBalance;
address lender;
IERC20 token;
}

// The full list of borrows that have occured in the


current transaction.
Borrow[] public transient borrows;

// The user borrowing. Borrower is able to call #borrow to


release tokens.
address public transient borrower;

/// @notice Entry Point. Start borrowing from the users


that have approved this contract.
function startLoan() external {
require(borrower == address(0)); // prevent reentrance

// TSTORE it!
borrower = msg.sender;

/// Hand control to the caller so they can start


borrowing tokens
IStartCallback(msg.sender).start();

// At this point `msg.sender` should have returned any


tokens that
// were borrowed to each lender. Check this and revert
if not!
for (uint256 i = 0; i < borrows.length; i++) {
Borrow transient borrow = borrows[i]; // TLOAD!
require(
borrow.token.balanceOf(borrow.lender) >=
borrow.lenderStartingBalance,
'You must pay back the person you borrowed
from!'
);
}

borrows.length = 0; // this doesn't actually work in


recent solidity versions for storage arrays, but we only need
to set the length of the array, you can also use TSTORE
directly

borrower = address(0); // clearing this allows it to


be called again in the same transaction
}

// Only callable by `borrower`. Used to borrow tokens.


function borrow(
address from,
IERC20 token,
uint256 amount,
address to
) external {
require(msg.sender == borrower, 'Must be called from
within the IStartCallback#start');

// TSTORE what has been borrowed


borrows.push(Borrow({lenderStartingBalance:
token.balanceOf(from), lender: from, token: token}));

token.transferFrom(from, to, amount);


}
}

The user calls NonCustodialFlashLoans#startLoan , and control is handed


back to them using the IStartCallback#start callback.
The user can then borrow as much of any token they want, from any user that
has sent approval, within this callback by
calling NonCustodialFlashLoans#borrow .
The contract uses transient storage to track what has been borrowed and check
the tokens are returned at the end.
Alternately The SLOAD/SSTORE’s make this pattern gas-infeasible without
transient storage.
Chainlink CCIP
Chainlik have addded to cross chain possibilities with CCIP.
Features

It has support for token transfers and arbitrary messaging.


From their documentation
"CCIP provides a single, simple, and elegant interface through which dApps and
web3 entrepreneurs can securely meet all their cross-chain needs, including
token transfers and arbitrary messaging."
With Chainlink CCIP, you can:
Transfer supported tokens
Send messages (any data)
Send messages and tokens
The CCIP receiver can be a
Smart contract that implements CCIPReceiver.sol
EOA (only tokens will be received)
Tooling support
Starter kits are available for Hardhat and Foundry
A explorer is available
Example Projects
A number of examples are available including an NFT minter project
This allows to to call a function on the source blockchain, CCIP calls a function
on the target blockchain to mint an NFT to msg.sender
An example calling a minting contract on Avalanche :
npx hardhat cross-chain-mint language-bash

--source-minter <SOURCE_MINTER_ADDRESS>
--source-blockchain ethereumSepolia
--destination-blockchain avalancheFuji
--destination-minter <DESTNATION_MINTER_ADDRESS>
--pay-fees-in Native
Huff
Repo
"Huff enables the construction of EVM assembly macros - blocks of bytecode
that can be rigorously tested and evaluated. Macros can themselves be
composed of Huff macros.
Huff doesn't hide the workings of the EVM behind syntactic sugar. In fact, Huff
doesn't hide anything at all. Huff does not have variables, instead directly
exposing the EVM's program stack to the developer to be directly manipulated."
Huff supports tables of jump destinations integrated directly into the contract
bytecode. This is to enable efficient program execution flows by using jump
tables instead of conditional branching.
A series of blog posts about Huff
Documentation is here
Series of tutorials
Getting started
Huff project template
See https://docs.huff.sh/get-started/project-quickstart/#using-the-template
Compiling the contract
see https://docs.huff.sh/get-started/compiling/#compiling-contracts-with-the-
huff-compiler
Example approve

Example getting storage values

Resources
Huffmate
Plugin for VSCode
Compiler built in rust
huff-rs
Foundry Library

Library
Install with
curl -L get.huff.sh | bash
forge install huff-language/foundry-huff

Huff Macros
There are only two fundamental building blocks to a Huff program:
Macros
Jump tables (and packed jump tables)
Example Macros
template <p1,p2>
#define macro POINT_DOUBLE = takes(3) returns(3) {
<p1> dup3 callvalue shl
swap3 dup4 mulmod
<p2> dup2 callvalue shl
dup2 dup1 dup1 dup4 dup10
mulmod dup2 sub swap8
dup1 mulmod 0x03 mul
dup2 dup2 dup1
mulmod dup9 callvalue shl add swap8
dup9 add mulmod swap3 mulmod add swap2
<p2> swap2 mulmod <p1> sub
}

Huff Jump tables


See Documentation
From docs :
Jump Tables are a convenient way to create switch cases in your Huff contracts.
Each jump table consists of jumpdest program counters (PCs), and it is written
to your contract's bytecode. These jumpdest PCs can be codecopied into
memory, and the case can be chosen by finding a jumpdest at a particular
memory pointer (i.e. 0x00 = case 1, 0x20 = case 2, etc.). This allows for a single
jump rather than many conditional jumps.
There are two different kinds of Jump Tables in Huff: Regular and Packed .
Regular Jump Tables store jumpdest PCs as full 32 byte words, and packed
Jump Tables store them each as 2 bytes. Therefore, packed jumptables are
cheaper to copy into memory, but they are more expensive to pull a PC out of
due to the bitshifting required. The opposite is true for Regular Jump Tables.
There are two builtin functions related to jumptables.
__tablestart(TABLE)
Pushes the program counter (PC) of the start of the table passed to the stack.
__tablesize(TABLE)
Pushes the code size of the table passed to the stack.
Example
// Define a function
#define function switchTest(uint256) pure returns (uint256)

// Define a jump table containing 4 pcs


#define jumptable SWITCH_TABLE {
jump_one jump_two jump_three jump_four
}

#define macro SWITCH_TEST() = takes (0) returns (0) {


// Codecopy jump table into memory @ 0x00
__tablesize(SWITCH_TABLE) // [table_size]
__tablestart(SWITCH_TABLE) // [table_start, table_size]
0x00
codecopy

0x04 calldataload // [input_num]

// Revert if input_num is not in the bounds of [0, 3]


dup1 // [input_num, input_num]
0x03 lt // [3 < input_num, input_num]
err jumpi

// Regular jumptables store the jumpdest PCs as full words,


// so we simply multiply the input number by 32 to determine
// which label to jump to.
0x20 mul // [0x20 * input_num]
mload // [pc]
jump // []

jump_one:
0x100 0x00 mstore
0x20 0x00 return
jump_two:
0x200 0x00 mstore
0x20 0x00 return
jump_three:
0x300 0x00 mstore
0x20 0x00 return
jump_four:
0x400 0x00 mstore
0x20 0x00 return
err:
0x00 0x00 revert
}

#define macro MAIN() = takes (0) returns (0) {


// Identify which function is being called.
0x00 calldataload 0xE0 shr
dup1 __FUNC_SIG(switchTest) eq switch_test jumpi

// Revert if no function matches


0x00 0x00 revert

switch_test:
SWITCH_TEST()
}

Constants in Huff
#define constant NUM = 0x420
#define constant HELLO_WORLD = 0x48656c6c6f2c20576f726c6421
#define constant FREE_STORAGE = FREE_STORAGE_POINTER()

In order to push a constant to the stack, use bracket notation: [CONSTANT]


Custom Errors
Custom errors can be defined and used by the __ERROR builtin to push the left-
padded 4 byte error selector to the stack.
Jump Labels
Jump Labels are a simple abstraction included into the language to make
defining and referring to JUMPDEST s more simple for the developer.
#define macro MAIN() = takes (0) returns (0) {
// Store "Hello, World!" in memory
0x48656c6c6f2c20576f726c6421
0x00 mstore // ["Hello, World!"]

// Jump to success label, skipping the revert statement


success // [success_label_pc, "Hello, World!"]
jump // ["Hello, World!"]
// Revert if this point is reached
0x00 0x00 revert

// Labels are defined within macros or functions, and are


designated
// by a word followed by a colon. Note that while it may
appear as if
// labels are scoped code blocks due to the indentation, they
are simply
// destinations to jump to in the bytecode. If operations
exist below a label,
// they will be executed unless the program counter is altered
or execution is
// halted by a `revert`, `return`, `stop`, or `selfdestruct`
opcode.
success:
0x00 mstore
0x20 0x00 return
}

You might also like