Skip to content
LogoLogo

Utilities

Create2

std/utils/create2.lithe. computeAddress(salt, initCodeHash, deployer) is where a CREATE2 from deployer lands, and computeAddressHere(salt, initCodeHash) is where defer create with that salt would put a contract from this one. initCodeHash is the hash of the created contract's creation code, with its deploy arguments appended: Crypto.keccak256(Evm.creationCode(C)) for a contract C without arguments, a constant.

The file also declares FailedDeployment, which the deployers below raise when a salted creation fails.

Create

std/utils/create.lithe. computeAddress(deployer, nonce) is where a plain CREATE from deployer lands when its nonce is nonce, as Solady's LibRLP.computeAddress computes it: the hash of the RLP list of the two. A contract's nonce starts at 1, and each creation moves it on, so the contract's first defer create lands at Create.computeAddress(Evm.self(), 1). Any nonce works, up to 32 bytes.

Clones

std/utils/clones.lithe. ERC-1167 minimal proxies, as OpenZeppelin's Clones makes them: a 45-byte contract that delegates every call to an implementation, so each instance costs a small creation instead of a copy of the implementation. code(implementation) is a clone's creation code, for defer create(code):

import "std/list";
import "std/utils/create2";
import "std/utils/clones";
 
contract Factory is {
  use Create2;
  use Clones;
 
  storage is record {
    implementation : address;
    instances : list(address);
  }
 
  external function open() is {
    defer create(Clones.code(storage.implementation)) then (instance : address) {
      List.push(storage.instances, instance);
    };
  }
 
  external function openAt(const salt : bytes32) : address
    raises FailedDeployment
  is {
    const instance : address := Clones.cloneDeterministic(storage.implementation, salt);
    List.push(storage.instances, instance);
  } with instance
}

A clone with arguments carries them after its code, where the implementation reads them with Clones.argsOf(Evm.self()): fixed configuration, such as a token and an owner, with no storage to initialize.

Function
code(implementation) : bytesa clone's creation code
codeWithArgs(implementation, args) : bytesthe same, with args after the clone's code; raises CloneArgumentsTooLong past 24531 bytes
cloneDeterministic(implementation, salt) : addresscreates a clone with CREATE2, and answers where it lands; raises FailedDeployment if it is already there
cloneDeterministic(implementation, salt, value) : addressthe same, sending value wei to the clone
cloneDeterministicWithArgs(implementation, args, salt), (implementation, args, salt, value)the same, with arguments
predict(implementation, salt, deployer), predictWithArgs(implementation, args, salt, deployer)where deployer would create the clone
predictHere(implementation, salt) : addresswhere this contract would
argsOf(instance) : bytesthe arguments a clone was created with; empty for a clone without them

SSTORE2

std/utils/sstore2.lithe. Data kept as the code of a contract, as Solady's SSTORE2 keeps it: writing costs a creation, about 200 gas a byte, and reading is one EXTCODECOPY, far cheaper than storage for data of more than a few words that never changes. The code starts with STOP, so a call to it does nothing. The data is at most 24575 bytes.

import "std/utils/create2";
import "std/utils/sstore2";
 
contract Art is {
  use Create2;
  use SSTORE2;
 
  storage is record {
    pointer : address;
  }
 
  external function store(const image : bytes) is {
    defer create(SSTORE2.code(image)) then (pointer : address) {
      storage.pointer := pointer;
    };
  }
 
  external view function image() : bytes is
    SSTORE2.read(storage.pointer)
}
Function
code(data) : bytesthe creation code that stores data
writeCounterfactual(data, salt) : addressstores data with CREATE2, and answers the pointer; raises FailedDeployment if the same data was stored with the salt before
predictCounterfactual(data, salt, deployer) : addresswhere deployer would store it
read(pointer) : bytesthe data; panics if pointer has no code

SSTORE2.read(p)[start:end] reads the whole data first. Evm.code(p)[1 + start:1 + end] copies only the range.

Create3

std/utils/create3.lithe. Addresses that depend only on the deployer and a salt, not on the creation code, as Solady's CREATE3 makes them: the same salt gives the same address on every chain, whatever the contract's code or constructor arguments. A CREATE2 makes a 16-byte proxy at a salted address, and the proxy creates the contract with its first CREATE.

import "std/evm";
import "std/utils/create2";
import "std/utils/create3";
 
contract Deployer is {
  use Create2;
  use Create3;
 
  storage is record {
    last : address;
  }
 
  external payable function deployAt(const code : bytes; const salt : bytes32) : address
    raises FailedDeployment
  is {
    storage.last := Create3.deployDeterministic(code, salt, uint(Evm.value()));
  } with storage.last
 
  external view function predict(const salt : bytes32) : address is
    Create3.predictHere(salt)
}
Function
deployDeterministic(code, salt) : addresscreates code, and answers its address; raises FailedDeployment if the salt was used or the creation fails
deployDeterministic(code, salt, value) : addressthe same, sending value wei to the new contract
predict(salt, deployer) : addresswhere deployer would create a contract with salt
predictHere(salt) : addresswhere this contract would
proxyCodeHash() : bytes32the hash of the proxy's creation code

Nonces

std/utils/nonce.lithe. Per-account counters that only move forward, for replay protection: each signed message names the next nonce, and using it moves the counter on.

Function
nonces(owner) : uintthe external: the next nonce
useNonce(owner) : uintreturns the next nonce and moves the counter on
useCheckedNonce(owner, nonce)the same, raising InvalidAccountNonce unless nonce is the next one

Checkpoints

std/utils/checkpoint.lithe. A history of values, each from a key onward, such as a block number or a timestamp: voting power, balances at a snapshot, or any value you need to read as of the past. A Trace holds a list of Checkpoints, each a uint48 key and a uint208 value in one storage slot, laid out as OpenZeppelin's Checkpoints.Trace208. A trace can be a storage field or a map's value, and each function takes either: Checkpoints.latest(storage.votes[account]).

import "std/math/math";
import "std/evm";
import "std/utils/checkpoint";
 
contract Supply is {
  use Math;
  use Checkpoints;
 
  storage is record {
    history : Trace;
    total : uint;
  }
 
  external function grow(const amount : uint)
    raises CheckpointUnorderedInsertion
  is {
    storage.total := storage.total + amount;
    Checkpoints.push(storage.history, uint48(Evm.blockNumber()), uint208(storage.total));
  }
 
  external view function totalAt(const block : uint) : uint is
    uint(Checkpoints.upperLookup(storage.history, uint48(block)))
}

A lookup is a binary search over the sorted history, so it costs a few reads however long the history is.

Function
push(history, key, value)keys must not go down; a second push at the newest key replaces its value
latest(history)the newest value, or zero
length(history), at(history, i)the checkpoints, oldest first; at past the end panics, as a list index does
upperLookup(history, key)the value as of key: the last checkpoint at or before it, or zero
upperLookupRecent(history, key)the same, faster for a recent key: it checks the newest square root of the history first

ERC7821

std/accounts/erc7821.lithe. A minimal batch executor, as Solady's ERC7821: execute(mode, executionData) runs a list of calls in order, each a target, a value, and calldata, and a failed call reverts the batch with the callee's own revert data. A call to Address.zero calls the account itself. Only the account may execute, which is what an EIP-7702 delegated EOA does when it sends a transaction to itself: approve and swap in one transaction.

import "std/accounts/erc7821";
import "std/interfaces/erc7821";
 
contract Delegate is {
  use ERC7821 exposing IERC7821;
 
  storage is record {}
 
  payable receive is {
    skip
  }
}

execute is payable, and each call's value comes out of the account's balance. receive lets the account take plain transfers once its code is delegated. Two modes are supported, as supportsExecutionMode answers:

ModeexecutionData
0x01000000000000000000…abi.encode(calls), calls a (address,uint256,bytes)[]
0x01000000000078210001…abi.encode(calls), or abi.encode(calls, opData) with extra bytes

Bytes 2 to 5 and 10 to 31 of a mode are free. Another mode raises UnsupportedExecutionMode, and another caller AccountUnauthorized(caller), both OpenZeppelin's. The module's execute does not read opData. Malformed executionData reverts before any call.

To let others submit batches, such as a relayer carrying a signature in opData, write your own execute from the pieces. Here a batch runs when the account's own key signed it with the next nonce:

import "std/accounts/erc7821";
import "std/crypto/ecdsa";
import "std/utils/nonce";
import "std/interfaces/erc7821";
import "std/abi";
import "std/evm";
import "std/crypto";
 
contract Account is {
  use ERC7821 exposing {
    view function supportsExecutionMode(mode : bytes32) : bool;
  }
  use ECDSA;
  use Nonces exposing {
    view function nonces(owner : address) : uint;
  }
 
  storage is record {}
 
  payable receive is {
    skip
  }
 
  external payable function execute(const mode : bytes32; const executionData : bytes)
    raises UnsupportedExecutionMode | AccountUnauthorized | ECDSAInvalidSignature
      | ECDSAInvalidSignatureLength | ECDSAInvalidSignatureS | forwarded
  is {
    const id : uint := ERC7821.executionModeId(mode);
    assert(id <> 0, UnsupportedExecutionMode);
    const calls : ERC7821Calls := Abi.decode(executionData, ERC7821Calls);
    const signature : bytes := ERC7821.opDataOf(id, executionData);
    if signature.length <> 0 then {
      const digest : bytes32 := ECDSA.toEthSignedMessageHash(Crypto.keccak256(Abi.encode(
        Evm.chainId(), Evm.self(), Nonces.useNonce(Evm.self()), calls
      )));
      assert(ECDSA.recoverSignature(digest, signature) = Evm.self(), AccountUnauthorized(Evm.sender()));
    } else {
      assert(Evm.sender() = Evm.self(), AccountUnauthorized(Evm.sender()));
    };
    ERC7821.executeCalls(calls);
  }
}
Piece
ERC7821Call, ERC7821Callsa call, { to; value; data }, and a list of them: decode a batch with Abi.decode(executionData, ERC7821Calls)
executionModeId(mode) : uint1 for a batch, 2 for a batch with optional opData, 0 for an unsupported mode
opDataOf(id, executionData) : bytesa batch's opData, or empty when id is not 2 or there is none
executeCalls(calls)defers each call, in order, with its value; a failed one reverts with its revert data, so this raises forwarded

Batches of batches

Solady's third mode, 0x01000000000078210002…, runs a batch of batches: executionData is abi.encode(batches), a bytes[] whose every element is a batch in mode 0x01000000000078210001…, with or without its own opData. It is useful for batches signed by different signers. ERC7821Batches, in the same file, adds it. It is opt-in, so an executor without it doesn't pay for it:

import "std/accounts/erc7821";
import "std/interfaces/erc7821";
 
contract Delegate is {
  use ERC7821;
  use ERC7821Batches exposing IERC7821Batches;
 
  storage is record {}
 
  payable receive is {
    skip
  }
}

IERC7821Batches is IERC7821 with Solady's BatchOfBatchesDecodingError, which execute raises where Solady's does: an offset past 64 bits, or a list or batch that runs past the end of executionData. The module's execute authorizes each batch as ERC7821 does, by the caller being the account.

Every batch is decoded and authorized before any call runs, since a transition's checks come before its calls. Solady runs each batch before it decodes the next, so there a batch's calls can change how a later batch is authorized, for example by rotating the signer. Here they can't, and a malformed batch anywhere reverts before any call.

An account that authorizes each batch with its own opData writes execute from the pieces. A single batch is executionData[0 : executionData.length]:

import "std/accounts/erc7821";
import "std/interfaces/erc7821";
import "std/abi";
import "std/crypto";
 
contract Signed is {
  error BadOpData;
 
  use ERC7821;
  use ERC7821Batches exposing {
    view function supportsExecutionMode(mode : bytes32) : bool;
  }
 
  storage is record {
    key : bytes32;
  }
 
  external payable function execute(const mode : bytes32; const executionData : bytes)
    raises UnsupportedExecutionMode | BatchOfBatchesDecodingError | BadOpData | forwarded
  is {
    const id : uint := ERC7821Batches.executionModeId(mode);
    assert(id >= 2, UnsupportedExecutionMode);
    const n : uint := if id = 3 then ERC7821Batches.batchCount(executionData) else 1;
    for i : uint in 0 .. n do {
      var start : uint := 0;
      var finish : uint := executionData.length;
      if id = 3 then {
        (start, finish) := ERC7821Batches.batch(executionData, i);
      } else {
        skip
      };
      const calls : ERC7821Calls := Abi.decode(executionData[start:finish], ERC7821Calls);
      const opData : bytes := ERC7821Batches.opDataIn(executionData, start, finish);
      assert(Crypto.keccak256(opData) = storage.key, BadOpData);
      ERC7821.executeCalls(calls);
    }
  }
}
Piece
executionModeId(mode) : uintas ERC7821's, and 3 for a batch of batches
batchCount(executionData) : uinthow many batches a batch of batches holds
batch(executionData, i) : (uint, uint)where batch i lies: executionData[start:finish]
opDataIn(executionData, start, finish) : bytesthe opData of the batch executionData[start:finish], or empty

Abi.decode(executionData[start:finish], ERC7821Calls) checks the batch where it lies in the calldata, with no copy, and each call reads its target, value, and data from there.

BuildRuntime bytesexecute, 2 callsbatch of 2 batches of 2 calls
ERC78215244,218
ERC7821 and ERC7821Batches1,0804,3767,856
Solady ERC78216733,8175,457

Gas is execution gas, calls to an account with no code. The extra over Solady is checking every call's encoding before the first one runs, as decoding would, where Solady reads each call unchecked as it makes it; a batch of batches also queues its calls until every batch is checked.

BlockHash

std/utils/block_hash.lithe. block_hash(n) is the hash of block n as Solady's BlockHashLib.blockHash finds it: Evm.block_hash(n) for the last 256 blocks, and before them a query of the EIP-2935 history contract at 0x0000F90827F1C53a10cb7A02335B175320002935, which keeps the last 8191. It is zero for this block and later ones, for blocks older than the history, and on a chain without the contract. history() is the contract's address.

import "std/utils/block_hash";
import "std/evm";
 
contract Beacon is {
  use BlockHash;
 
  storage is record {
    committed : uint;
  }
 
  external function commit() is {
    storage.committed := Evm.blockNumber();
  }
 
  (* A seed from a block that was in the future when it was committed to. *)
  external view function seed() : bytes32 is
    BlockHash.blockHash(storage.committed + 1)
}

Strings

std/utils/strings.lithe. Searching strings, as Solady's LibString searches them. Offsets count bytes, not characters. Changing case, escaping, searching, and replacing are built-ins: String.lower, String.upper, String.escapeJson, String.escapeHtml, String.indexOf, and String.replace.

Function
indexOf(subject, needle), indexOf(subject, needle, from)where needle first starts, at or after from, or Uint.max if it isn't there. An empty needle is found at from, or at the end if from is past it
contains(subject, needle) : boolwhether needle is in subject
startsWith(subject, needle) : bool, endsWith(subject, needle) : boolwhether subject starts or ends with needle
replace(subject, needle, replacement) : stringevery needle, left to right, replaced. An empty needle puts replacement before each byte and at the end
import "std/utils/strings";
import "std/string";
import "std/crypto";
 
contract Names is {
  use Strings;
 
  error NameHasSpace;
 
  storage is record {
    names : map(bytes32, string);
  }
 
  external function register(const name : string)
    raises NameHasSpace
  is {
    assert(not Strings.contains(name, " "), NameHasSpace);
    storage.names[Crypto.keccak256(String.lower(name))] := name;
  }
 
  external pure function slug(const title : string) : string is
    Strings.replace(String.lower(title), " ", "-")
}

DateTime

std/utils/date_time.lithe. Gregorian dates in UTC, as Solady's DateTimeLib computes them. Months and days count from 1, and weekdays from Monday, 1, to Sunday, 7, as in ISO 8601. Times are timestamps. Dates from 1970-01-01 to the year 0xffffffff are supported; outside them, as in Solady, results are unspecified. isSupportedDate checks a date.

import "std/utils/date_time";
import "std/evm";
 
contract Subscription is {
  use DateTime;
 
  storage is record {
    paidUntil : map(address, timestamp);
  }
 
  (* A month from now, or from the end of the current month paid for. 31 January
     and one month is 28 or 29 February. *)
  external function renew() is {
    const now : timestamp := Evm.blockTimestamp();
    const from : timestamp := if storage.paidUntil[Evm.sender()] > now then storage.paidUntil[Evm.sender()] else now;
    storage.paidUntil[Evm.sender()] := DateTime.addMonths(from, 1);
  }
 
  external view function renewsOn(const who : address) : (uint, uint, uint) is
    DateTime.timestampToDate(storage.paidUntil[who])
}
Function
dateToEpochDay(year, month, day) : uint, epochDayToDate(day) : (uint, uint, uint)days since 1970-01-01, and back
dateToTimestamp(year, month, day), timestampToDate(t)the same for the start of the day, as a timestamp
dateTimeToTimestamp(year, month, day, hour, minute, second), timestampToDateTime(t)with the time of day
isLeapYear(year) : bool, daysInMonth(year, month) : uintdaysInMonth is 0 for a month past 12
weekday(t) : uint1 for Monday to 7 for Sunday
isSupportedDate(year, month, day) : boolwhether the date exists and is in the supported range
addYears(t, n), subYears(t, n), addMonths(t, n), subMonths(t, n)the same day of the month and time of day, or the month's last day if it has fewer
diffYears(from, to) : uint, diffMonths(from, to) : uintcalendar years or months between: 1 from 31 December to 1 January. Panics if to is before from

Days, hours, minutes, and seconds need no library: t + 3days, and (later - earlier) / 1days.