Skip to content
LogoLogo

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.

ModuleImport
Evmimport "std/evm";
Cryptoimport "std/crypto";
Abiimport "std/abi";
Bytesimport "std/bytes";
Stringimport "std/string";
Base64import "std/base64";
Listimport "std/list";
Uintimport "std/uint";
Intimport "std/int";
Addressimport "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-inTypeSolidity
Evm.sender()addressmsg.sender
Evm.value()weimsg.value. Only in payable functions
Evm.calldata()bytesmsg.data
Evm.selector()bytes4msg.sig

Transaction

Built-inTypeSolidity
Evm.txOrigin()addresstx.origin. Do not use for authorization
Evm.txGasPrice()weitx.gasprice

Block

Built-inTypeSolidity
Evm.blockNumber()uintblock.number
Evm.blockTimestamp()timestampblock.timestamp
Evm.blockCoinbase()addressblock.coinbase
Evm.blockBaseFee()weiblock.basefee
Evm.blockPrevrandao()bytes32block.prevrandao
Evm.blockGasLimit()uintblock.gaslimit
Evm.chainId()uintblock.chainid
Evm.block_hash(n : uint)bytes32blockhash(n)
Evm.blobHash(i : uint)bytes32blobhash(i)
Evm.blobBaseFee()weiblock.blobbasefee

Accounts

Built-inTypeSolidity
Evm.self()addressaddress(this)
Evm.balance(a : address)weia.balance
Evm.codeSize(a : address)uinta.code.length
Evm.codeHash(a : address)bytes32a.codehash
Evm.code(a : address)bytesa.code
Evm.gasLeft()uintgasleft()
Evm.facetAddress(M)addressthe address of the facet of a module M the contract uses delegated. A constant, in contract code only
Evm.creationCode(C)bytestype(C).creationCode, for a contract C that defer create can create. Usable in pure functions, and its hash is a constant

Sending ether

Built-inTypeSolidity
Evm.transfer(to : address, amount : wei)operationpayable(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-inTypeSolidity
Evm.call(to : address, data : bytes)operationto.call{value: v}(data), with with value = v
Evm.delegateCall(to : address, data : bytes)operationto.delegatecall(data)
Evm.staticCall(to : address, data : bytes)bytesto.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 n

Uint.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-inTypeSolidity
Crypto.keccak256(x : bytes | string)bytes32keccak256(x)
Crypto.sha256(x : bytes | string)bytes32sha256(x)
Crypto.ripemd160(x : bytes | string)bytes20ripemd160(x)
Crypto.ecrecover(hash : bytes32, v : uint, r : bytes32, s : bytes32)addressecrecover(...)

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-inTypePrecompile
Crypto.modExp(base : bytes, exponent : bytes, modulus : bytes)bytes, as long as modulus0x05, EIP-198
Uint.powMod(base : uint, exponent : uint, modulus : uint)uint0x05 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)))bool0x08, 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)bool0x0a, EIP-4844. commitment and proof are 48 bytes
Crypto.bls12G1Add(a : bytes, b : bytes), Crypto.bls12G2Add(a, b)bytes0x0b, 0x0d, EIP-2537
Crypto.bls12G1Msm(pairs : bytes), Crypto.bls12G2Msm(pairs)bytes0x0c, 0x0e. Each pair is a point, then a 32-byte scalar
Crypto.bls12Pairing(pairs : bytes)bool0x0f
Crypto.bls12MapFpToG1(fp : bytes), Crypto.bls12MapFp2ToG2(fp2 : bytes)bytes0x10, 0x11
Crypto.p256Verify(hash : bytes32, r : bytes32, s : bytes32, x : bytes32, y : bytes32)bool0x100, 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-inTypeSolidity
Abi.encode(a, b, ...)bytesabi.encode(a, b, ...)
Abi.encodePacked(a, b, ...)bytesabi.encodePacked(a, b, ...)
Abi.encodeWithSelector(sel : bytes4, a, ...)bytesabi.encodeWithSelector(sel, a, ...)
Abi.encodeWithSignature(sig : string, a, ...)bytesabi.encodeWithSignature(sig, a, ...)
Abi.encodeCall(Interface.method, a, ...)bytesabi.encodeCall(I.method, (a, ...)). Arguments are type-checked
Abi.decode(data : bytes, T)Tabi.decode(data, (T))
Abi.selector(Interface.method)bytes4I.method.selector. A constant. An overloaded method has no single selector: hash its signature with Crypto.keccak256
Abi.interfaceId(Interface)bytes4type(I).interfaceId: the XOR of its methods' selectors. A constant
Abi.supportsInterface(id : bytes4)boolwhether 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-inTypeSolidity
Bytes.concat(a, b, ...)bytesbytes.concat(a, b, ...). Arguments are bytes or bytesN
Bytes.word(b : bytes, offset : uint)bytes32the 32 bytes at offset. Panic(0x32) if they run past the end
Bytes.join(xs : list(bytes))bytesthe 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, ...)stringstring.concat(a, b, ...)
String.lower(s), String.upper(s)stringSolady's LibString.lower(s) and upper(s): ASCII letters recased, every other byte, UTF-8 included, as it was
String.join(xs : list(string))stringthe strings of xs one after another, as Bytes.join
String.escapeJson(s)stringLibString.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)stringLibString.escapeHTML(s): ", &, ', <, and > as &quot;, &amp;, &#39;, &lt;, and &gt;
String.indexOf(s, needle, from : uint)uintLibString.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)stringLibString.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():

ExpressionTypeSolidity
b[i]bytes1b[i], for b : bytes or any bytesN. Panic(0x32) if i is past the end
b[start:end], b[start:], b[:end]bytesb[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, stringbytes(s), string(b), with no copy
bytesN(b)bytesNbytesN(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 have

Base64

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-inTypeSolidity
Base64.encode(data)stringBase64.encode(data): the standard alphabet, padded with =
Base64.encodeUrl(data)stringBase64.encode(data, true, true): - and _ for + and /, no padding
Base64.encodeWith(data, fileSafe : bool, noPadding : bool)stringBase64.encode(data, fileSafe, noPadding)
Base64.decode(data)bytesBase64.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-inTypeSolidity
List.push(xs, v)statementxs.push(v): appends v to a dynamic list
List.pop(xs)statementxs.pop(): drops a dynamic list's last element. Panic(0x31) if it is empty
List.resized(xs, n : uint)the type of xsa 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 xs

Uint, 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-inTypeSolidity
Uint.maxuint, or the expected unsigned typetype(uint256).max
Int.min, Int.maxint, or the expected signed typetype(int256).min, .max
Address.zeroaddressaddress(0)
Uint.divRem(a, b)(T, T)(a / b, a % b)
Uint.mulDiv(a, b, d : uint)uintMath.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)uintmulmod(a, b, d), addmod(a, b, d), without overflow. Panic(0x12) if d is zero
Uint.popCount(x : uint)uintSolady's LibBit.popCount(x): how many bits are set
Uint.clz(x : uint)uintLibBit.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 bothunchecked { 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 boththe 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, intuint256(i), int256(n): the same 256 bits, unchecked. uint(i) and int(n) check
Uint.toString(n)stringStrings.toString(n): an unsigned integer of any width in decimal
Int.toString(n)stringStrings.toStringSigned(n): a signed integer in decimal, - first when negative
Uint.toHexString(n)stringStrings.toHexString(n): 0x and lowercase hex in the fewest whole bytes, 0x00 for zero
Uint.toHexString(n, length : uint)stringStrings.toHexString(n, length): exactly length bytes, zero-padded. Panic(0x11) if n doesn't fit
Address.toHexString(a)stringStrings.toHexString(a): 0x and 40 lowercase hex digits
Address.toChecksumString(a)stringStrings.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.