@kimlikdao/evmscript
v0.0.3
Published
Write gas-optimized smart contracts in TypeScript
Maintainers
Readme
EvmScript is TypeScript that compiles to lean EVM bytecode. It is an experimental library and framework for generating, testing, and deploying gas-efficient Ethereum Virtual Machine programs from the TypeScript ecosystem.
It is built around a typed stack algebra: EVM programs are composed from
Fragments whose stack effects are checked at compile time, then assembled into
compact bytecode. For each statement, EvmScript searches for the minimum-cost
stack choreography instead of relying on hand-written DUP/SWAP sequences.
Why EvmScript?
EvmScript is for EVM-side hot paths where bytecode size, stack choreography, and per-call gas dominate the cost of an operation. It is a good fit for small, specialized verifiers, proxies, payout programs, settlement transactions, liquidation bots, auctions, bridges, and other systems where shaving gas and bytes compounds over many transactions.
The goal is to keep the authoring experience TypeScript-native while giving the compiler room to produce bytecode that is hard to match by hand. Optimal stack dances are often non-obvious even in small programs; EvmScript models that problem directly and searches the state space.
Install
bun add @kimlikdao/evmscriptRequires Bun and TypeScript. The project currently tests against Bun and TypeScript from the package lockfile.
Authoring Model
Today, EvmScript programs are written in ordinary .ts files using the lowered
library API. The upcoming .tsevm syntax will work like a TypeScript language
extension, similar in spirit to .tsx: domain-specific syntax is transpiled
back into regular TypeScript library calls.
For example, a future .tsevm Merkle verifier could be written as:
const verifyMerkle = evm (
hash: Data,
index: Uint,
proof: Data[32]
): Bool => {
unroll for (const level in range(32)) {
hash = hashPair(proof[level], (index & 1) * 32, hash);
index = index >> 1;
}
return hash == sload<Data>(0);
}That author-facing syntax lowers into the library form used by the compiler today:
const verifyMerkle = inline(
{ hash: Data, index: Uint, proof: array(Data, depth) },
({ hash, index, proof }) => [
unrollFor(
[],
range(depth),
(level) => [
set(hash, hashPairAtOffset(
proof.at(level),
mul(bitAnd(index, 1), 32),
hash,
)),
set(index, shr(1, index)),
],
),
eq(hash, sload(0, Data)),
],
);Because programs live directly inside TypeScript, metaprogramming stays ordinary TypeScript: functions, loops, arrays, sorting, grouping, fixtures, tests, and deployment scripts all come from the same toolchain.
Core Ideas
- Typed stack algebra: bytecode fragments carry typed stack signatures, so composition fails early when stack values do not line up.
- Expression and statement lowering: TypeScript builders lower expressions, assignments, loops, and function bodies into fragment composition.
- Solver-guided assembly: the binder converts each statement into an abstract stack problem, then a direct strategy or A* search finds the optimal opcode path for that modeled problem while preserving future-live values.
- TypeScript-native deployment: compilation produces EVM bytecode as a
Uint8Array, ready to deploy withviem,ethers,wagmi,@kimlikdao/lib, or the Ethereum tooling you already use.
Examples
examples/merkle.ts: a Merkle proof verifierexamples/batchSend.ts: grouped batch ETH sendsexamples/proxies.ts: small proxy programs
Development
bun install
bunx --no-install tsc -p .
bun testThe GitHub workflow runs both the TypeScript typecheck and the Bun unit tests.
Documentation
The docs in docs/ cover the compiler model in more detail:
