Introduction
Plank is an EVM-native programming language for writing smart contracts, giving you direct access to every opcode as a builtin function, without inline assembly blocks, or separate syntax. At the same time, it provides modern language features like compile-time evaluation (comptime), generics, structs, modules, and first-class functions. By combining low-level control with high-level ergonomics, Plank lets you write expressive code that still compiles to efficient bytecode.
Under the hood, Plank compiles to EVM bytecode via Sensei IR (SIR), a language-agnostic intermediate representation designed for fast compilation and EVM-specific optimizations.
Why Plank
The EVM tooling stack has lagged behind. “Stack too deep” errors, slow compile times, and unreliable editor support shouldn’t exist in 2026. Plank aims to raise the bar on expressivity, compilation speed, and developer experience.
Comptime is Plank’s superpower that makes this design possible. Instead of relying on separate macro systems, template metaprogramming, or external code generation, Plank uses a single unified mechanism for generics, type introspection, constant computation, and metaprogramming. Inspired by Zig, this keeps the language simple while still enabling powerful abstractions, without adding runtime cost.
Status
Plank is still in its early stages: the compiler, standard library, and language features are actively evolving. If you run into bugs, have ideas for improvement, or want to get involved, open an issue on GitHub or reach out on X.
Getting Started
Installation
Install Plank using plankup:
curl -L https://install.plankevm.org | bash
This installs the plankup tool, which manages your Plank installation. It downloads the latest binary to ~/.plank/bin/, installs local documentation, and can optionally configure syntax highlighting for VS Code, Cursor, and VSCodium.
To update Plank to the latest version, run:
plankup
Other Editors
Neovim
For Neovim check out the plank.nvim extension.
Zed
For Zed check out the zed-plank extension.
Other Editors
For additional editor support, a tree-sitter grammar is available. You can find a description of Plank’s at grammar in the monorepo if you wish to add support for another editor.
Your First Contract
Create a file called magic_number.plk:
import std::constructor::return_runtime;
const MAGIC_NUMBER_SLOT = 0;
const GET_SELECTOR = 0x6d4ce63c;
init {
let buf = @malloc_zeroed(32);
@evm_codecopy(buf, @init_end_offset(), 32);
@evm_sstore(MAGIC_NUMBER_SLOT, @mload32(buf));
return_runtime();
}
run {
let selector = @evm_calldataload(0) >> 224;
if selector == GET_SELECTOR {
let buf = @malloc_uninit(32);
@mstore32(buf, @evm_sload(MAGIC_NUMBER_SLOT));
@evm_return(buf, 32);
} else {
@evm_revert(@malloc_uninit(0), 0);
}
}
Out of the box Plank contracts are very bare bones, giving you access to two entry points: init
and run. init becomes your contract’s initcode and runs once at deployment, while run is the
entrypoint to your runtime logic. Note nothing is implicit, by default you’ll need to use something like
std::constructor::return_runtime in your init to ensure your contract’s
runtime code is set to run.
In the above example, when the init block runs, it reads the initial magic number from the arguments, stores it, and returns the runtime bytecode. The run block executes on every call: it extracts the function selector from the first 4 bytes of calldata and executes the get() method if the selector matches GET_SELECTOR; otherwise, it reverts.
Compile it:
plank build magic_number.plk
Browsing the Documentation
Plank also installs the documentation locally. Open it in your browser with:
plank doc
To jump directly to a specific topic:
plank doc comptime
Alternatively feed it to your LLM by pointing it to the ~/.plank/share/doc/src/ folder which contains the docs in their original markdown form.
What Makes Plank Different
Plank gives smart contract developers more direct control over how their code is written and executed. It exposes low-level EVM behavior, removes hidden abstractions, and keeps most mechanisms explicit.
This section highlights the key differences you’ll encounter coming from Solidity, Vyper, or Huff, and how those differences affect how you write contracts.
Contract Structure: init and run
Plank contracts are split into two explicit blocks:
init- deployment logicrun- runtime logic
Unlike Solidity, there’s no implicit constructor or fallback. Deployment and execution are separated by design, so it’s always clear what runs when.
Comptime
Plank supports compile-time execution through comptime. It enables features like generics, ABI encoding, and type-driven code generation with zero runtime cost.
See Comptime for details.
Direct EVM Access
Every EVM opcode is exposed as a builtin function. There’s no separate assembly or inline assembly: opcodes are called directly in your code via builtins such as @evm_sload, @evm_sstore, @evm_caller, and @evm_keccak256. Low-level operations become composable building blocks, reusable and abstractable with the same flexibility as high-level code.
let caller = @evm_caller();
let amount = @evm_calldataload(4);
let slot = map_slot_hash(caller, BALANCE_SLOT);
@evm_sstore(slot, amount);
Operator Semantics
Arithmetic operators in Plank are checked by default: +, -, and * revert on overflow and underflow. When wrapping behavior is required, for example for modular arithmetic on storage slots, use the %-suffixed variants (+%, -%, *%):
let sum = a + b; // reverts on overflow
let slot = base +% offset; // wraps around
Division has two rounding modes, since division can be floored or ceiled:
7 -/ 2 == 3; // floor division
7 +/ 2 == 4; // ceiling division
By requiring an explicit choice, Plank makes the rounding direction immediately visible in the code, rather than relying on a single / operator whose rounding behavior is easy to forget or overlook.
Bitwise operators (|, ^, &, <<, >>, ~) and comparison operators (==, !=, <, >, <=, >=) follow standard conventions.
Logical operators are and and or, instead of && and ||.
Memory Management
Plank simplifies low-level memory management via the @malloc_uninit and @malloc_zeroed builtins, with the compiler controlling the final memory layout and allocation lifetimes.
This design enables optimizations such as redundant allocation removal, allocation merging and reordering, and promotion of dynamic allocations to static slots. With full visibility into memory usage, the compiler can produce memory layouts that minimize memory footprint, avoid unnecessary reads and writes, and safely spill variables from the EVM stack to memory when needed, thus eliminating the possibility of “stack too deep” errors.
While some of these optimizations are still a work in progress, the model is designed to support them.
const map_slot_hash = fn (key: u256, base_slot: u256) u256 {
let buf = @malloc_uninit(64);
@mstore32(buf, key);
@mstore32(buf +% 32, base_slot);
@evm_keccak256(buf, 64)
};
Modules Over Inheritance
Plank has no contract inheritance. Instead, code is organized through a module system with explicit imports.
import std::abi::{abi_encode, abi_decode};
import std::math::max;
There are no virtual functions, no inheritance linearization, and no diamond problem. Composition is done by importing and using functionality directly.
ABI Encode and Decode
Plank’s standard library provides ABI encoding and decoding utilities. With comptime, the compiler inspects struct fields and generates the correct encoding logic automatically - you only pass the type and data.
import std::abi::abi_encode;
const Transfer = struct { to: u256, amount: u256 };
let encoded = abi_encode(Transfer, transfer);
@evm_return(encoded.ptr, encoded.len);
No manual calldata slicing, no hardcoded offsets - just define your struct, and the standard library handles the rest.
First-Class Functions
Functions in Plank are values at comptime - you can store them in variables, pass them as arguments, and return them from other functions.
Patterns that require special syntax in Solidity, like access control modifiers, can be expressed as comptime functions in Plank:
const require_owner = fn(comptime action: function) void {
if @evm_caller() != OWNER { revert_empty(); }
action();
};
Comptime
Comptime lets you execute code during compilation rather than at runtime, using the same syntax you already know. The result is embedded directly into the final bytecode with no runtime cost.
You can use comptime in two ways:
- A
comptimeblock evaluates an expression at compile time
const SECONDS_PER_YEAR = comptime { 365 * 24 * 3600 };
- A
comptimeparameter tells the compiler to specialize a function for each distinct value it is called with:
const DOUBLE = fn(comptime x: u256) u256 {
x * 2
};
In many cases, compile-time evaluation happens implicitly. When all values in an expression are known at compile time, the compiler evaluates it automatically. For example, 1 + 2 becomes 3 in the bytecode with no runtime cost.
What You Get From Comptime
Reduced On-Chain Cost
The EVM charges for computation, not compilation. Anything you move from runtime to compile time is effectively free. The SECONDS_PER_YEAR example above evaluates entirely at comptime and the result is directly embedded in the bytecode without executing the multiplication on chain.
Readability and Auditability
If you see 7919 in a codebase, do you know it’s the 1000th prime? With comptime, you don’t need to guess, you can write const P_1000 = comptime { nth_prime(1000) }; instead. Both produce the same bytecode, but only one makes it clear what the value represents.
Zero-Cost Generics
comptime parameters allow the compiler to specialize functions per type. No runtime type checks, no unused code paths. One definition, used with any type - the compiler generates optimized code for each.
const max = fn(comptime T: type, a: T, b: T) T {
if a > b { a } else { b }
};
Compile-Time Introspection
At comptime, everything is a value, including struct types. This lets you define and use functions that perform introspection on them, such as inspecting the number and types of fields.
This enables code generation at compile time, eliminating boilerplate and allowing functions to adapt to the exact structure they operate on.
For example, this is how ABI encoding is handled through the standard library:
const Transfer = struct { to: u256, amount: u256 };
let encoded = abi_encode(Transfer, transfer);
As long as your struct is defined, this call never changes. Add a new field to Transfer, and the encoding updates automatically - no manual fixes, no risk of missing fields.
The same pattern can be extended to things like ERC712 type hashes, storage layout, and event encoding: define the type once, and let the compiler keep everything in sync.
Examples
The following examples demonstrate real contract patterns you can write in Plank today, broken down by their key components.
Note: Plank is still evolving, and future releases will make these patterns simpler and more powerful. Check the roadmap for planned improvements.
ERC20 Token
A standard ERC20 implementation covering the core patterns used in Plank:
- Contract structure:
init(deployment) andrun(runtime) - Direct EVM access via opcode builtins (
@evm_sload,@evm_sstore,@evm_log3, etc.) - Explicit memory management with
@malloc_uninitand@mstore32
import std::abi::{abi_encode};
import std::membytes::{membytes, membytes_from_ptr};
import std::storage::map_slot_hash;
import std::constructor::return_runtime;
const TOTAL_SUPPLY = 1000000;
const TOTAL_SUPPLY_SLOT = 0;
const BALANCE_SLOT_BASE = 1;
const ALLOWANCE_SLOT_BASE = 2;
const BALANCE_OF_SELECTOR = 0x70a08231;
const TOTAL_SUPPLY_SELECTOR = 0x18160ddd;
const ALLOWANCE_SELECTOR = 0xdd62ed3e;
const TRANSFER_SELECTOR = 0xa9059cbb;
const APPROVE_SELECTOR = 0x095ea7b3;
const TRANSFER_FROM_SELECTOR = 0x23b872dd;
const DECIMALS_SELECTOR = 0x313ce567;
const NAME_SELECTOR = 0x06fdde03;
const SYMBOL_SELECTOR = 0x95d89b41;
const TRANSFER_TOPIC = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
const APPROVAL_TOPIC = 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925;
init {
@evm_sstore(TOTAL_SUPPLY_SLOT, TOTAL_SUPPLY);
let caller = @evm_caller();
let slot = map_slot_hash(caller, BALANCE_SLOT_BASE);
let buf = @malloc_uninit(32);
@evm_sstore(slot, TOTAL_SUPPLY);
@mstore32(buf, TOTAL_SUPPLY);
@evm_log3(buf, 32, TRANSFER_TOPIC, 0, caller);
return_runtime();
}
run {
let selector = @evm_calldataload(0) >> 224;
if selector == BALANCE_OF_SELECTOR {
balance_of(@evm_calldataload(4));
} else if selector == TOTAL_SUPPLY_SELECTOR {
total_supply();
} else if selector == TRANSFER_SELECTOR {
transfer();
} else if selector == ALLOWANCE_SELECTOR {
allowance();
} else if selector == APPROVE_SELECTOR {
approve();
} else if selector == TRANSFER_FROM_SELECTOR {
transfer_from();
} else if selector == DECIMALS_SELECTOR {
decimals();
} else if selector == NAME_SELECTOR {
name();
} else if selector == SYMBOL_SELECTOR {
symbol();
} else {
@evm_revert(@malloc_uninit(0), 0);
}
}
const balance_of = fn (address: u256) never {
let slot = map_slot_hash(address, BALANCE_SLOT_BASE);
let buf = @malloc_uninit(32);
let balance = @evm_sload(slot);
@mstore32(buf, balance);
@evm_return(buf, 32);
};
const total_supply = fn () never {
let buf = @malloc_uninit(32);
let supply = @evm_sload(TOTAL_SUPPLY_SLOT);
@mstore32(buf, supply);
@evm_return(buf, 32);
};
const transfer = fn () never {
let to = @evm_calldataload(4);
let amount = @evm_calldataload(36);
transfer_balance(@evm_caller(), to, amount);
let buf = @malloc_uninit(32);
@mstore32(buf, 1);
@evm_return(buf, 32);
};
const allowance = fn () never {
let owner = @evm_calldataload(4);
let spender = @evm_calldataload(36);
let slot = map_slot_hash(spender, map_slot_hash(owner, ALLOWANCE_SLOT_BASE));
let allowance = @evm_sload(slot);
let buf = @malloc_uninit(32);
@mstore32(buf, allowance);
@evm_return(buf, 32);
};
const approve = fn () never {
let spender = @evm_calldataload(4);
let amount = @evm_calldataload(36);
let caller = @evm_caller();
let slot = map_slot_hash(spender, map_slot_hash(caller, ALLOWANCE_SLOT_BASE));
@evm_sstore(slot, amount);
let buf = @malloc_uninit(32);
@mstore32(buf, amount);
@evm_log3(buf, 32, APPROVAL_TOPIC, caller, spender);
@mstore32(buf, 1);
@evm_return(buf, 32);
};
const transfer_from = fn () never {
let from = @evm_calldataload(4);
let to = @evm_calldataload(36);
let amount = @evm_calldataload(68);
let allowance_slot = map_slot_hash(@evm_caller(), map_slot_hash(from, ALLOWANCE_SLOT_BASE));
let allowance_val = @evm_sload(allowance_slot);
if allowance_val < amount {
@evm_revert(@malloc_uninit(0), 0);
}
@evm_sstore(allowance_slot, allowance_val - amount);
transfer_balance(from, to, amount);
let buf = @malloc_uninit(32);
@mstore32(buf, 1);
@evm_return(buf, 32);
};
const decimals = fn () never {
let buf = @malloc_uninit(32);
@mstore32(buf, 18);
@evm_return(buf, 32);
};
const name = fn () never {
let ptr = @malloc_uninit(10);
@mstore10(ptr, 0x506c616e6b546f6b656e);
let encoded = abi_encode(membytes, membytes_from_ptr(ptr, 10));
@evm_return(encoded.ptr, encoded.len);
};
const symbol = fn () never {
let ptr = @malloc_uninit(3);
@mstore3(ptr, 0x504c4b);
let encoded = abi_encode(membytes, membytes_from_ptr(ptr, 3));
@evm_return(encoded.ptr, encoded.len);
};
const transfer_balance = fn (from: u256, to: u256, amount: u256) void {
let slot = map_slot_hash(from, BALANCE_SLOT_BASE);
let balance_from = @evm_sload(slot);
if balance_from < amount {
@evm_revert(@malloc_uninit(0), 0);
}
@evm_sstore(slot, balance_from - amount);
let slot = map_slot_hash(to, BALANCE_SLOT_BASE);
let balance_to = @evm_sload(slot);
@evm_sstore(slot, balance_to + amount);
let buf = @malloc_uninit(32);
@mstore32(buf, amount);
@evm_log3(buf, 32, TRANSFER_TOPIC, from, to);
};
Imports
The contract uses a few standard library utilities:
std::storage::map_slot_hash- computes mapping storage slots usingkeccak256(key, base_slot)std::constructor::return_runtime- returnsruntimebytecode frominitstd::abi::abi_encode- encodes values into ABI formatstd::membytes::{membytes, membytes_from_ptr}- utilities for working with raw memory slices
Constants
Storage slots and function selectors are defined as constants at the top of the file. There is no compiler-managed storage layout yet, so storage slots are defined explicitly and mapping slots are derived using map_slot_hash.
Function selectors and event topics follow standard EVM conventions.
The init Block
The init block runs once at deployment. It sets the total supply in slot 0, credits the deployer’s balance, emits a Transfer event from address zero to the deployer, and returns the runtime bytecode via return_runtime().
The run Block and Dispatch
Every call to the deployed contract enters the run block. It extracts the 4-byte selector from the first calldata word and matches it against known selectors using an if / else if chain. Unrecognized selectors revert.
Reading and Writing Storage
Reading a balance is a single @evm_sload call with a slot derived from map_slot_hash(address, BALANCE_SLOT_BASE). Writing the balance is a @evm_sstore with the same slot.
Events
Events are emitted using @evm_log3 (three indexed topics plus data):
@evm_log3(buf, 32, TRANSFER_TOPIC, from, to);
ABI Encoding
let ptr = @malloc_uninit(10);
@mstore10(ptr, 0x506c616e6b546f6b656e);
let encoded = abi_encode(membytes, membytes_from_ptr(ptr, 10));
@evm_return(encoded.ptr, encoded.len);
This pattern writes raw data into memory, wraps it as membytes, and passes it to abi_encode to produce the output returned to the caller.
Minimal Proxy (ERC1167)
A factory contract that deploys cheap clones of an existing contract. Each clone delegates all calls to the same implementation, significantly reducing deployment cost.
Beyond the ERC20 example, this demonstrates:
- Raw bytecode construction: building ERC1167 proxy bytecode in memory
- Contract deployment using
createandcreate2 - Comptime generics: a single
deploy_clonefunction that works with or without a salt
import std::constructor::return_runtime;
const CLONE_SELECTOR = 0x8124b78e;
const CLONE_DETERMINISTIC_SELECTOR = 0xb86b2ceb;
const CLONE_CREATED_TOPIC = 0xbe2f3d28fdeb5839123d65fd47ec2f5915c715d2b527b9e229123706fdecfc85;
init {
return_runtime();
}
run {
let selector = @evm_calldataload(0) >> 224;
if selector == CLONE_SELECTOR {
clone();
} else if selector == CLONE_DETERMINISTIC_SELECTOR {
clone_deterministic();
} else {
@evm_revert(@malloc_uninit(0), 0);
}
}
const clone = fn () never {
deploy_clone(void, {});
};
const clone_deterministic = fn () never {
let salt = @evm_calldataload(36);
deploy_clone(u256, salt);
};
const deploy_clone = fn (comptime SaltT: type, salt: SaltT) never {
let address = @evm_calldataload(4);
let len = 32 + 20 + 15;
let buf = @malloc_uninit(len);
@mstore32(buf +% (len - 32), 0x5af43d82803e903d91602b57fd5bf3);
@mstore32(buf +% (len - 32 - 15), address);
@mstore32(buf +% (len - 32 - 15 - 20), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73);
let address = if SaltT == void {
@evm_create(0, buf +% (32 - 20), 55)
} else if SaltT == u256 {
@evm_create2(0, buf +% (32 - 20), 55, salt)
} else {
// Jank compile-time error.
let _unsupported_clone_type_error: u256 = true;
};
@mstore32(buf, address);
@evm_log1(buf, 32, CLONE_CREATED_TOPIC);
@evm_return(buf, 32);
};
Dispatch
The contract exposes two entry points for proxy deployment:
clone- deploys a proxy at a non-deterministic address usingcreateclone_deterministic- deploys a proxy at a deterministic address usingcreate2
Both read the implementation address from calldata and delegate to deploy_clone.
Building the Proxy Bytecode
The ERC1167 minimal proxy is a fixed 55-byte contract assembled directly in memory:
let len = 32 + 20 + 15;
let buf = @malloc_uninit(len);
@mstore32(buf +% (len - 32), 0x5af43d82803e903d91602b57fd5bf3);
@mstore32(buf +% (len - 32 - 15), address);
@mstore32(buf +% (len - 32 - 15 - 20), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73);
The bytecode consists of three parts: a fixed prefix (20 bytes), the implementation address (20 bytes), and a fixed suffix (15 bytes).
We write the components in backwards order as this allows us to use more
efficient @mstore32 calls instead of @mstore20 / @mstore15 that aren’t native in the EVM and therefore cost more.
Such optimizations will be handled automatically by the standard library in the future.
Comptime Generic Dispatch
Both clone and clone_deterministic share the same deploy_clone helper. The difference is the comptime type argument passed to it: clone uses void, meaning no salt specialization is required, while clone_deterministic uses u256 for the salt type.
const clone = fn () never {
deploy_clone(void, {});
};
const clone_deterministic = fn () never {
let salt = @evm_calldataload(36);
deploy_clone(u256, salt);
};
The helper takes a comptime type SaltT (void or u256) and a value of that type. Because SaltT is comptime, the compiler specializes each branch and removes runtime checks, allowing shared logic to be written without any runtime branching cost.
In theory, deploy_clone could be called with any type, e.g., deploy_clone(bool, some_value_of_type_bool). However, this does not make sense in the context of the proxy. To restrict usages, any type other than void and u256 triggers a compile-time error via the else branch:
const deploy_clone = fn (comptime SaltT: type, salt: SaltT) never {
let address = if SaltT == void {
@evm_create(0, buf, 55)
} else if SaltT == u256 {
@evm_create2(0, buf, 55, salt)
} else {
// compile-time error.
let _unsupported_clone_type_error: u256 = true;
};
};
Merkle Airdrop
A token airdrop contract where eligible recipients are encoded in a Merkle tree. Users claim their allocation by submitting a Merkle proof, which the contract verifies on-chain before marking the claim. Token distribution is not handled.
This example demonstrates:
- Iterating over calldata using a
whileloop to process proof elements - Handling variable-length calldata for runtime-sized proofs
import std::storage::map_slot_hash;
import std::constructor::return_runtime;
const CLAIM_SELECTOR = 0x3d13f874;
const MERKLE_ROOT_SELECTOR = 0x2eb4a7ab;
const HAS_CLAIMED_SELECTOR = 0x73b2e80e;
const CLAIMED_TOPIC = 0xd8138f8a3f377c5259ca548e70e4c2de94f129f5a11036a15b69513cba2b426a;
const MERKLE_ROOT_SLOT = 0;
const HAS_CLAIMED_SLOT_BASE = 1;
init {
let args_buf = @malloc_zeroed(32);
@evm_codecopy(args_buf, @init_end_offset(), 32);
let merkle_root = @mload32(args_buf);
@evm_sstore(MERKLE_ROOT_SLOT, merkle_root);
return_runtime();
}
run {
let selector = @evm_calldataload(0) >> 224;
if selector == CLAIM_SELECTOR {
claim();
} else if selector == MERKLE_ROOT_SELECTOR {
merkle_root();
} else if selector == HAS_CLAIMED_SELECTOR {
has_claimed();
} else {
@evm_revert(@malloc_uninit(0), 0);
}
}
const claim = fn () never {
let buf = @malloc_uninit(64);
let slot = get_merkle_slot();
let claimed = @evm_sload(slot);
if claimed != 0 {
@evm_revert(@malloc_uninit(0), 0);
}
let address = @evm_calldataload(4);
let amount = @evm_calldataload(36);
let offset = 4 + @evm_calldataload(68);
let proof_length = @evm_calldataload(offset);
@mstore32(buf, address);
@mstore32(buf +% 32, amount);
let mut node = @evm_keccak256(buf +% 12, 52);
let mut i = 0;
while i < proof_length {
let proof_element = @evm_calldataload(offset + 32 + i * 32);
if node <= proof_element {
@mstore32(buf, node);
@mstore32(buf +% 32, proof_element);
} else {
@mstore32(buf, proof_element);
@mstore32(buf +% 32, node);
}
node = @evm_keccak256(buf, 64);
i = i + 1;
}
let root = @evm_sload(MERKLE_ROOT_SLOT);
if node != root {
@evm_revert(@malloc_uninit(0), 0);
}
@evm_sstore(slot, 1);
@mstore32(buf, amount);
@evm_log2(buf, 32, CLAIMED_TOPIC, address);
@evm_return(@malloc_uninit(0), 0);
};
const merkle_root = fn () never {
let buf = @malloc_uninit(32);
@mstore32(buf, @evm_sload(MERKLE_ROOT_SLOT));
@evm_return(buf, 32);
};
const has_claimed = fn () never {
let buf = @malloc_uninit(64);
let slot = get_merkle_slot();
let claimed = @evm_sload(slot);
@mstore32(buf, claimed);
@evm_return(buf, 32);
};
const get_merkle_slot = fn () u256 {
let address = @evm_calldataload(4);
map_slot_hash(address, HAS_CLAIMED_SLOT_BASE)
};
Claiming
The claim function performs the core verification logic:
- Checks if the caller has already claimed by reading their storage slot
- Reads the address, amount, and Merkle proof from calldata
- Computes the leaf hash from the address and amount
- Iterates over the proof, sorting each node pair before hashing to compute the next layer
- Compares the final computed root with the stored root; if valid, marks the claim and emits an event
The proof is processed in a while loop, reading each element from calldata by its offset:
let mut i = 0;
while i < proof_length {
let proof_element = @evm_calldataload(offset + 32 + i * 32);
...
i = i + 1;
}
Leaf Computation
The leaf is computed by packing the address (20 bytes) and amount (32 bytes) into a manually allocated buffer:
let buf = @malloc_uninit(64);
@mstore32(buf, address);
@mstore32(buf +% 32, amount);
let mut node = @evm_keccak256(buf +% 12, 52);