Hashing and ABI encoding
evs mirrors Solidity’s three encoding-and-hashing primitives on the builder, byte-exact with what solc produces:
| Op | Solidity equivalent | Result |
|---|---|---|
s.encode(...values) |
abi.encode(...) |
Expr<'bytes'> |
s.encodePacked(...values) |
abi.encodePacked(...) |
Expr<'bytes'> |
s.keccak256(...values) |
keccak256(abi.encode(...)) |
Expr<'bytes32'> |
All three take staged values — Exprs, Tuple handles, MutArray handles — and at least one of them. A raw literal has no unambiguous ABI type, so lift it with s.lit first:
import { evscript, t } from '@maxencerb/evs';
const transferTopic = evscript({ name: 'transferTopic', args: [] }, (s) => { const sig = s.lit(t.string, 'Transfer(address,address,uint256)'); return s.return({ topic: s.keccak256(sig) });});s.keccak256 — the standard-encoding default
Section titled “s.keccak256 — the standard-encoding default”s.keccak256(...values) hashes the standard ABI encoding of its values — keccak256(abi.encode(...)). It accepts everything s.encode accepts, so structs, nested arrays, and string[] hash directly; a struct value gives the EIP-712-style leaf hash:
import { evscript, t } from '@maxencerb/evs';
const Pair = t.struct({ token: t.address, fee: t.uint24 });
const structHash = evscript({ name: 'structHash', args: [Pair] }, (s, pair) => { // keccak256(abi.encode(pair)) — the EIP-712-style leaf hash, no composition needed return s.return({ h: s.keccak256(pair) });});One exception: a single bytes or string value is hashed directly — its raw payload, no encoding, no copy — which is exactly Solidity’s keccak256(someBytes). That rule is what makes the explicit compositions below hash precisely the bytes they name (s.keccak256(s.encode(...)) never double-encodes).
A single word hashes its full 32-byte standard encoding regardless of width: a uint8 and a uint256 both hash 32 padded bytes, matching keccak256(abi.encode(x)) in Solidity.
Packed hashing is explicit
Section titled “Packed hashing is explicit”abi.encodePacked is non-standard, and hashing it implicitly is a footgun (see the ambiguity caveat below) — so evs never packs unless you say so. The packed hash is the composition:
import { evscript, t } from '@maxencerb/evs';
const pairKey = evscript( { name: 'pairKey', args: [t.address, t.address, t.uint24] }, (s, tokenA, tokenB, fee) => { // keccak256(abi.encodePacked(tokenA, tokenB, fee)) — 20 + 20 + 3 bytes const key = s.keccak256(s.encodePacked(tokenA, tokenB, fee)); // bytes32 → uint256 view when you need a number return s.return({ key, keyAsUint: key.asUint256() }); },);This works because s.encodePacked(...) produces a bytes value and a single bytes value hashes raw — the hash covers exactly the packed encoding.
s.encode — standard ABI encoding
Section titled “s.encode — standard ABI encoding”s.encode produces the head/tail abi.encode bytes of its values and accepts the whole v0 type vocabulary: words, string/bytes, arrays (including string[], uint256[][], and arrays of structs), and t.struct/t.tuple values — bare Tuple and MutArray handles are fine, just like in s.return:
import { evscript, t } from '@maxencerb/evs';
const Pair = t.struct({ token: t.address, fee: t.uint24 });
const encodeOrder = evscript( { name: 'encodeOrder', args: [t.uint256, t.array(t.uint128)] }, (s, id, amounts) => { const pair = s.tuple(Pair, { token: '0x00000000000000000000000000000000deadbeef', fee: 500n }); return s.return({ blob: s.encode(id, pair, amounts) }); },);The output matches abi.encode (and viem’s encodeAbiParameters) byte for byte: 32-byte head words, dynamic members as offsets relative to the start of the encoding, tails in declaration order.
s.encodePacked — packed encoding
Section titled “s.encodePacked — packed encoding”Packed encoding follows Solidity’s rules exactly:
- sub-32-byte word types pack unpadded:
uintN/intNtakeN / 8bytes,address20,bool1,bytesNexactlyN; stringandbytescontribute their raw payload with no length prefix;- a word-element array (
uint16[],address[], …) packs each element padded to 32 bytes — Solidity pads array elements even in packed mode.
The shapes Solidity rejects in abi.encodePacked — structs, nested arrays, and string[]/bytes[] — are rejected at recording time with an EvsTypeError, so every packed encoding evs produces is one solc could have produced. Use s.encode for those types.
import { evscript, t } from '@maxencerb/evs';
const packed = evscript({ name: 'packed', args: [t.uint8, t.string, t.address] }, (s, v, name, who) => { // 1 byte ++ raw utf8 ++ 20 bytes — no padding, no length words return s.return({ blob: s.encodePacked(v, name, who) });});Note the usual packed caveat (inherited from Solidity, not fixed by evs): packed encoding is ambiguous for adjacent dynamic values — encodePacked("ab", "c") equals encodePacked("a", "bc"). This is why packed hashing is opt-in: don’t hash attacker-controlled adjacent dynamic values with s.keccak256(s.encodePacked(...)); the s.keccak256(...) default (standard encoding) is unambiguous.
Costs and placement
Section titled “Costs and placement”Both encoders materialize a fresh bytes value in script memory (evs never frees memory, so an encode inside a loop allocates every iteration — compile() warns with the LOOP_ALLOCATION diagnostic). s.keccak256 over a single bytes/string performs no allocation at all — it hashes the existing value in place. None of these operations can revert.