Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ERC20 Token

A standard ERC20 implementation covering the core patterns used in Plank:

  • Contract structure: init (deployment) and run (runtime)
  • Direct EVM access via opcode builtins (@evm_sload, @evm_sstore, @evm_log3, etc.)
  • Explicit memory management with @malloc_uninit and @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 using keccak256(key, base_slot)
  • std::constructor::return_runtime - returns runtime bytecode from init
  • std::abi::abi_encode - encodes values into ABI format
  • std::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.