Built-ins
Lithe has no magic globals. Execution context comes from Evm, hashing from Crypto, and encoding from Abi. Every call names where its value comes from, so a search for Evm. finds every place a contract depends on its environment.
Each of these is a module of the standard library, declared in its own file, and a file imports the ones it uses as it imports any module. A file that doesn't import std/evm doesn't read the caller, the block, or balances, and its first lines say so.
| Module | Import |
|---|---|
Evm | import "std/evm"; |
Crypto | import "std/crypto"; |
Abi | import "std/abi"; |
Bytes | import "std/bytes"; |
String | import "std/string"; |
Base64 | import "std/base64"; |
List | import "std/list"; |
Uint | import "std/uint"; |
Int | import "std/int"; |
Address | import "std/address"; |
Using one without its import is the error any unknown name is, with the import to add:
storage is record {
owner : address;
}
external function claim() is {
storage.owner := Evm.sender();
}unknown name `Evm`
`Evm` is declared in `std/evm`: `import "std/evm";`An alias qualifies the module as it does any other: after import "std/evm" as Chain;, the caller is Chain.Evm.sender(). A file that doesn't import std/evm can declare a module of its own named Evm.
The compiler implements these modules' members. Each file declares its members with intrinsic, with a signature where Lithe can write one, and a comment that editors show on hover. A member whose arguments vary in number or type, such as Abi.encode, or whose width comes from the context, such as Uint.max, has no signature, and its row below says what it takes. Only the standard library declares an intrinsic.
The language's own names need no import: types, literals, conversions such as uint(x), Some and None, and assert, fail, and forward.
Evm
Execution context, from import "std/evm";. Readable in view and stateful functions, and not in pure functions.
Message
| Built-in | Type | Solidity |
|---|---|---|
Evm.sender() | address | msg.sender |
Evm.value() | wei | msg.value. Only in payable functions |
Evm.calldata() | bytes | msg.data |
Evm.selector() | bytes4 | msg.sig |
Transaction
| Built-in | Type | Solidity |
|---|---|---|
Evm.txOrigin() | address | tx.origin. Do not use for authorization |
Evm.txGasPrice() | wei | tx.gasprice |
Block
| Built-in | Type | Solidity |
|---|---|---|
Evm.blockNumber() | uint | block.number |
Evm.blockTimestamp() | timestamp | block.timestamp |
Evm.blockCoinbase() | address | block.coinbase |
Evm.blockBaseFee() | wei | block.basefee |
Evm.blockPrevrandao() | bytes32 | block.prevrandao |
Evm.blockGasLimit() | uint | block.gaslimit |
Evm.chainId() | uint | block.chainid |
Evm.block_hash(n : uint) | bytes32 | blockhash(n) |
Evm.blobHash(i : uint) | bytes32 | blobhash(i) |
Evm.blobBaseFee() | wei | block.blobbasefee |
Accounts
| Built-in | Type | Solidity |
|---|---|---|
Evm.self() | address | address(this) |
Evm.balance(a : address) | wei | a.balance |
Evm.codeSize(a : address) | uint | a.code.length |
Evm.codeHash(a : address) | bytes32 | a.codehash |
Evm.code(a : address) | bytes | a.code |
Evm.gasLeft() | uint | gasleft() |
Evm.facetAddress(M) | address | the address of the facet of a module M the contract uses delegated. A constant, in contract code only |
Evm.creationCode(C) | bytes | type(C).creationCode, for a contract C that defer create can create. Usable in pure functions, and its hash is a constant |
Sending ether
| Built-in | Type | Solidity |
|---|---|---|
Evm.transfer(to : address, amount : wei) | operation | payable(to).call{value: amount}("") |
Evm.transfer builds an operation, so it is only valid after defer, in a stateful function: defer Evm.transfer(to, amount);. See Queries and deferred calls.
Raw calls
| Built-in | Type | Solidity |
|---|---|---|
Evm.call(to : address, data : bytes) | operation | to.call{value: v}(data), with with value = v |
Evm.delegateCall(to : address, data : bytes) | operation | to.delegatecall(data) |
Evm.staticCall(to : address, data : bytes) | bytes | to.staticcall(data) |
Evm.call and Evm.delegateCall are deferred, and a continuation gets the return data as bytes. Evm.staticCall runs with query. See Raw calls.
Reading Evm.value() outside a payable function is an error, so a function cannot rely on ether it could never receive:
import "std/evm";
storage is record {
n : uint;
}
external function go() is {
const x : wei := Evm.value();
skip
}external function `go` reads `Evm.value()`; write `external payable function`Target fork
Evm.osaka is a bool constant: whether the code is built for Osaka or later, as --evm-version sets. Unlike the rest of Evm, a pure function can read it. An if on it builds only the branch for the target. Both branches are still checked:
import "std/evm";
import "std/uint";
storage is record {
n : uint;
}
external pure function bitLength(const x : uint) : uint is
if Evm.osaka then 256 - Uint.clz(x) else {
var n : uint := 0;
for _ : uint in 0 .. 256 until x >> n = 0 do {
n := n + 1;
}
} with nUint.clz works on every fork. The branch is worth writing only when the older fork has a cheaper way than the built-in's own fallback.
Crypto
Hash functions and signature recovery, from import "std/crypto";. Usable in any function, pure ones included.
| Built-in | Type | Solidity |
|---|---|---|
Crypto.keccak256(x : bytes | string) | bytes32 | keccak256(x) |
Crypto.sha256(x : bytes | string) | bytes32 | sha256(x) |
Crypto.ripemd160(x : bytes | string) | bytes20 | ripemd160(x) |
Crypto.ecrecover(hash : bytes32, v : uint, r : bytes32, s : bytes32) | address | ecrecover(...) |
To hash several values, encode them first: Crypto.keccak256(Abi.encode(a, b)).
The hash of a string or hex literal is a constant: Crypto.keccak256("MINTER_ROLE") compiles to the 32-byte word, as Solidity's keccak256("MINTER_ROLE") in a constant does. Write typehashes and role ids this way rather than pasting the hash.
Precompiles
Every precompiled contract is a built-in, given its input as the precompile lays it out. Like the hashes, they are usable in pure functions.
| Built-in | Type | Precompile |
|---|---|---|
Crypto.modExp(base : bytes, exponent : bytes, modulus : bytes) | bytes, as long as modulus | 0x05, EIP-198 |
Uint.powMod(base : uint, exponent : uint, modulus : uint) | uint | 0x05 on words. Panic(0x12) if modulus is zero |
Crypto.bn254Add(a : (uint, uint), b : (uint, uint)) | (uint, uint) | 0x06, EIP-196 |
Crypto.bn254Mul(p : (uint, uint), s : uint) | (uint, uint) | 0x07, EIP-196 |
Crypto.bn254Pairing(pairs : list((uint, uint, uint, uint, uint, uint))) | bool | 0x08, EIP-197. Each pair is (g1X, g1Y, g2XIm, g2XRe, g2YIm, g2YRe) |
Crypto.blake2f(rounds : uint32, h : list(bytes32, 2), m : list(bytes32, 4), t : bytes16, final : bool) | list(bytes32, 2) | 0x09, EIP-152. h, m, and t hold their little-endian words as the EIP lays them out |
Crypto.kzgPointEvaluation(versionedHash : bytes32, z : uint, y : uint, commitment : bytes, proof : bytes) | bool | 0x0a, EIP-4844. commitment and proof are 48 bytes |
Crypto.bls12G1Add(a : bytes, b : bytes), Crypto.bls12G2Add(a, b) | bytes | 0x0b, 0x0d, EIP-2537 |
Crypto.bls12G1Msm(pairs : bytes), Crypto.bls12G2Msm(pairs) | bytes | 0x0c, 0x0e. Each pair is a point, then a 32-byte scalar |
Crypto.bls12Pairing(pairs : bytes) | bool | 0x0f |
Crypto.bls12MapFpToG1(fp : bytes), Crypto.bls12MapFp2ToG2(fp2 : bytes) | bytes | 0x10, 0x11 |
Crypto.p256Verify(hash : bytes32, r : bytes32, s : bytes32, x : bytes32, y : bytes32) | bool | 0x100, EIP-7951 |
BLS12-381 points and field elements are bytes in EIP-2537's encoding: a field element is 64 bytes, a G1 point 128, and a G2 point 256.
A precompile that rejects its input, such as a point off its curve, reverts the transaction, except where the result is a bool: kzgPointEvaluation and p256Verify answer false for a proof or signature that does not verify. The pairings answer whether the product of pairings is one, and revert on malformed input.
Each call also checks the size of the answer. On a chain without the precompile, the call reaches an account with no code, which succeeds with no answer: that reverts, or reads as false, never as zeros. Crypto.p256Verify answers false on chains before EIP-7951; WebAuthn.verifySignature in std/crypto/webauthn falls back to Solady's verifier contract there.
The identity precompile, 0x04, copies its input, which a value already is. query Evm.staticCall(to, data) reaches any precompile directly.
Abi
ABI encoding and decoding, from import "std/abi";. Usable in any function, pure ones included.
| Built-in | Type | Solidity |
|---|---|---|
Abi.encode(a, b, ...) | bytes | abi.encode(a, b, ...) |
Abi.encodePacked(a, b, ...) | bytes | abi.encodePacked(a, b, ...) |
Abi.encodeWithSelector(sel : bytes4, a, ...) | bytes | abi.encodeWithSelector(sel, a, ...) |
Abi.encodeWithSignature(sig : string, a, ...) | bytes | abi.encodeWithSignature(sig, a, ...) |
Abi.encodeCall(Interface.method, a, ...) | bytes | abi.encodeCall(I.method, (a, ...)). Arguments are type-checked |
Abi.decode(data : bytes, T) | T | abi.decode(data, (T)) |
Abi.selector(Interface.method) | bytes4 | I.method.selector. A constant. An overloaded method has no single selector: hash its signature with Crypto.keccak256 |
Abi.interfaceId(Interface) | bytes4 | type(I).interfaceId: the XOR of its methods' selectors. A constant |
Abi.supportsInterface(id : bytes4) | bool | whether id is ERC-165's or one supports lists, as the generated supportsInterface answers. See ERC-165 |
Abi.decode takes a type name as its second argument: Abi.decode(data, uint), which decodes one value, as Solidity's abi.decode(data, (uint256)). Any ABI value can be decoded; name a list or record type first, as type ids is list(uint);, then Abi.decode(data, ids). A tuple of type names decodes one value of each, as abi.decode(data, (address, uint256)) does: const (who, amount) : (address, uint) := Abi.decode(data, (address, uint));. Malformed data reverts, with Solidity's checks on every offset, length, and word.
Abi.encode takes any ABI values, each one argument: a record or tuple is a Solidity tuple. Abi.encodePacked packs what Solidity's abi.encodePacked does: words, strings, bytes, and lists of words, each element padded to 32 bytes. It has no layout for records, tuples, nested lists, or lists of strings or bytes.
import "std/abi";
import "std/crypto";
import "std/evm";
storage is record {
digest : bytes32;
}
external function commit(const amount : uint; const salt : bytes32) is {
storage.digest := Crypto.keccak256(Abi.encode(Evm.sender(), amount, salt));
}
external pure function roundTrip(const n : uint) : uint is
Abi.decode(Abi.encode(n), uint)
external pure function transferCall(const to : address; const amount : uint) : bytes is
Abi.encodeWithSignature("transfer(address,uint256)", to, amount)Bytes and String
From import "std/bytes"; and import "std/string";.
| Built-in | Type | Solidity |
|---|---|---|
Bytes.concat(a, b, ...) | bytes | bytes.concat(a, b, ...). Arguments are bytes or bytesN |
Bytes.word(b : bytes, offset : uint) | bytes32 | the 32 bytes at offset. Panic(0x32) if they run past the end |
Bytes.join(xs : list(bytes)) | bytes | the values of xs one after another, copied once: build a long value by pushing its pieces onto a list, not by concatenating onto a growing one |
String.concat(a, b, ...) | string | string.concat(a, b, ...) |
String.lower(s), String.upper(s) | string | Solady's LibString.lower(s) and upper(s): ASCII letters recased, every other byte, UTF-8 included, as it was |
String.join(xs : list(string)) | string | the strings of xs one after another, as Bytes.join |
String.escapeJson(s) | string | LibString.escapeJSON(s): " and \ backslashed, control characters as \n, \t, and the like, or \u00XX. String.escapeJson(s, quoted : bool) is LibString.escapeJSON(s, quoted): in double quotes when quoted. A slice argument, s[i:j], is read in place, not copied |
String.escapeHtml(s) | string | LibString.escapeHTML(s): ", &, ', <, and > as ", &, ', <, and > |
String.indexOf(s, needle, from : uint) | uint | LibString.indexOf(s, needle, from): the byte offset where needle first starts, at from or later, or Uint.max. An empty needle is found at from, or at the end if from is past it |
String.replace(s, needle, replacement) | string | LibString.replace(s, needle, replacement): every needle, left to right, replaced. An empty needle puts replacement before each byte and at the end |
import "std/bytes";
import "std/string";
storage is record {
greeting : string;
}
external function greet(const name : string) is {
storage.greeting := String.concat("hello, ", name, "!");
}
external pure function tagged(const tag : bytes4; const data : bytes) : bytes is
Bytes.concat(tag, data)
external pure function attribute(const name : string; const value : string) : string is
String.concat("{\"trait_type\":\"", String.escapeJson(name), "\",\"value\":\"", String.escapeJson(value), "\"}")Searching strings is in the standard library's Strings. A slice passed to Bytes.concat or Crypto.keccak256 is read where it lies, not copied first.
bytes values index and slice as in Solidity. Each works on parameters, locals, stored values, and call results such as Evm.calldata():
| Expression | Type | Solidity |
|---|---|---|
b[i] | bytes1 | b[i], for b : bytes or any bytesN. Panic(0x32) if i is past the end |
b[start:end], b[start:], b[:end] | bytes | b[start:end] on calldata: a copy of the bytes from start up to end. Panic(0x32) if start > end or end is past the end |
bytes(s), string(b) | bytes, string | bytes(s), string(b), with no copy |
bytesN(b) | bytesN | bytesN(b): the first N bytes of b, zero past its end. From a bytesM, the first N bytes, or all M and then zeros |
A single byte can't be assigned: build a new value from slices with Bytes.concat.
import "std/abi";
import "std/crypto";
import "std/evm";
storage is record {
forwarder : address;
}
(* ERC-2771: a trusted forwarder appends the original sender to the calldata. *)
external view function msgSender() : address is {
const data : bytes := Evm.calldata();
var sender : address := Evm.sender();
if sender = storage.forwarder and data.length >= 20 then {
sender := address(bytes20(data[data.length - 20:]));
}
} with sender
(* The payload of a custom error `LowBalance(uint256)`, from its revert data. *)
external pure function lowBalance(const reason : bytes) : uint is {
var have : uint := 0;
if reason.length >= 36 and bytes4(reason) = bytes4(Crypto.keccak256("LowBalance(uint256)")) then {
have := Abi.decode(reason[4:], uint);
}
} with haveBase64
RFC 4648 base64, from import "std/base64";, with the same algorithm and lookup tables as Solady's Base64. Usable in any function, pure ones included. data is bytes or string.
| Built-in | Type | Solidity |
|---|---|---|
Base64.encode(data) | string | Base64.encode(data): the standard alphabet, padded with = |
Base64.encodeUrl(data) | string | Base64.encode(data, true, true): - and _ for + and /, no padding |
Base64.encodeWith(data, fileSafe : bool, noPadding : bool) | string | Base64.encode(data, fileSafe, noPadding) |
Base64.decode(data) | bytes | Base64.decode(data): either alphabet, padded or not, and , for 63 as in RFC 3501 |
Like Solady's, Base64.decode does not check its input. A character Solady's lookup table covers decodes as it does there: [ as 63, and other characters outside both alphabets below { as zero bits. So do { to DEL and the control characters, and a byte from 0x80 up decodes as its low seven bits.
import "std/base64";
import "std/string";
storage is record {
n : uint;
}
external pure function contractURI() : string is
String.concat("data:application/json;base64,", Base64.encode("{\"name\":\"Badge\"}"))List
From import "std/list";.
| Built-in | Type | Solidity |
|---|---|---|
List.push(xs, v) | statement | xs.push(v): appends v to a dynamic list |
List.pop(xs) | statement | xs.pop(): drops a dynamic list's last element. Panic(0x31) if it is empty |
List.resized(xs, n : uint) | the type of xs | a new list of xs's first n elements, padded with zero values to n: List.resized([], n) is new T[](n). Panic(0x41) if n is 2^64 or more |
xs is a stored list or a var, and List.push(xs, v); and List.pop(xs); are statements: they write xs as an assignment would, so a view function cannot push to or pop a stored list. A stored list clears the element's slots. To remove an element from the middle, move the last one into its place first:
import "std/list";
storage is record {
ids : list(uint);
}
external function remove(const i : uint) is {
storage.ids[i] := storage.ids[storage.ids.length - 1];
List.pop(storage.ids);
}List.resized is an expression, and works on any dynamic list: a parameter, a local, or a stored list, which it reads. The padding is the value unwritten storage reads as: zero words, empty strings, bytes, and lists, None, and a variant's first case. Sizing a list once and writing its elements costs less than pushing them one by one:
import "std/list";
storage is record { n : uint; }
external pure function squares(const n : uint) : list(uint) is {
var xs : list(uint) := List.resized([], n);
for i : uint in 0 .. n do {
xs[i] := i * i;
}
} with xsUint, Int, and Address
From import "std/uint";, import "std/int";, and import "std/address";. Constants take their width from the type the context expects. With no expected type, they are 256-bit.
| Built-in | Type | Solidity |
|---|---|---|
Uint.max | uint, or the expected unsigned type | type(uint256).max |
Int.min, Int.max | int, or the expected signed type | type(int256).min, .max |
Address.zero | address | address(0) |
Uint.divRem(a, b) | (T, T) | (a / b, a % b) |
Uint.mulDiv(a, b, d : uint) | uint | Math.mulDiv(a, b, d): a * b / d with the product in 512 bits. Panic(0x11) if the result doesn't fit, Panic(0x12) if d is zero |
Uint.mulMod(a, b, d : uint), Uint.addMod(a, b, d : uint) | uint | mulmod(a, b, d), addmod(a, b, d), without overflow. Panic(0x12) if d is zero |
Uint.popCount(x : uint) | uint | Solady's LibBit.popCount(x): how many bits are set |
Uint.clz(x : uint) | uint | LibBit.clz(x), Math.clz(x): the zero bits above the highest set bit, 256 for zero. One CLZ on Osaka |
Uint.wrappingAdd(a, b), Uint.wrappingSub(a, b), Uint.wrappingMul(a, b) | T, the unsigned type of both | unchecked { a + b } and the like: the result modulo 2^N for T's N bits |
Int.wrappingAdd(a, b), Int.wrappingSub(a, b), Int.wrappingMul(a, b) | T, the signed type of both | the same for signed types, in two's complement |
Int.divRem(a, b) | (T, T) | (a / b, a % b), truncating |
Uint.fromBits(i : int), Int.fromBits(n : uint) | uint, int | uint256(i), int256(n): the same 256 bits, unchecked. uint(i) and int(n) check |
Uint.toString(n) | string | Strings.toString(n): an unsigned integer of any width in decimal |
Int.toString(n) | string | Strings.toStringSigned(n): a signed integer in decimal, - first when negative |
Uint.toHexString(n) | string | Strings.toHexString(n): 0x and lowercase hex in the fewest whole bytes, 0x00 for zero |
Uint.toHexString(n, length : uint) | string | Strings.toHexString(n, length): exactly length bytes, zero-padded. Panic(0x11) if n doesn't fit |
Address.toHexString(a) | string | Strings.toHexString(a): 0x and 40 lowercase hex digits |
Address.toChecksumString(a) | string | Strings.toChecksumHexString(a): EIP-55 mixed case |
import "std/int";
import "std/string";
import "std/uint";
storage is record {
cap : uint8;
label : string;
}
external function name(const id : uint) is {
storage.label := String.concat("token #", Uint.toString(id));
}
external function reset() is {
storage.cap := Uint.max;
}
external pure function bounds() : (int16, int16) is
(Int.min, Int.max)
external pure function elapsed(const later : uint32; const earlier : uint32) : uint32 is
Uint.wrappingSub(later, earlier)Here storage.cap := Uint.max stores 255, because the target is a uint8, and elapsed(1, 3) is 4294967294: the difference wraps to 32 bits.
Some and None
Some(x) and None build options.
Conversions
A type name used as a function converts to it: uint(x), uint8(n), int(n), address(b), bytes20(a), wei(n), timestamp(n), and the names of your own nominal types. See Conversions.

