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.