Bytecode size and gas
Lithe's restrictions make its programs easier to compile, as well as easier to review. Across the benchmark suite, Lithe's runtime bytecode is about 32% smaller than the best of five optimized solc builds of the equivalent Solidity. Every one of the 51 contracts comes in smaller, most by 20–45%.
Runtime bytecode
Runtime bytecode size in bytes. Smaller is better. solc is 0.8.36, and each column is one optimizer configuration:
| contract | lithe | solc ir r1 | solc ir r200 | solc ir r10k | solc ir r1M | solc r200 | lithe vs best solc |
|---|---|---|---|---|---|---|---|
| base64 | 891 | 1301 | 1322 | 1655 | 1655 | 1170 | -23.8% |
| book | 1160 | 1247 | 1251 | 1766 | 1766 | 1585 | -7.0% |
| codec | 2596 | 2657 | 2652 | 3049 | 3619 | 3117 | -2.1% |
| collections | 627 | 1085 | 1098 | 1380 | 1775 | 1392 | -42.2% |
| counter | 198 | 331 | 336 | 489 | 489 | 408 | -40.2% |
| date_time | 1166 | 1225 | 1272 | 2492 | 2792 | 1860 | -4.8% |
| deployers | 1503 | 1916 | 1903 | 2617 | 2917 | 2877 | -21.0% |
| dialer | 1401 | 1803 | 1841 | 2315 | 2495 | 2229 | -22.3% |
| env | 81 | 128 | 128 | 213 | 213 | 137 | -36.7% |
| erc1155 | 5013 | 6610 | 6788 | 9315 | 9975 | 7072 | -24.2% |
| erc20 | 2244 | 3090 | 3135 | 4439 | 5159 | 3056 | -26.6% |
| erc20_full | 7382 | 10512 | 10747 | 13612 | 16021 | 10226 | -27.8% |
| erc4626 | 3075 | 3683 | 3735 | 4752 | 5352 | 4408 | -16.5% |
| erc6909 | 3326 | 4558 | 4653 | 5658 | 6565 | 4587 | -27.0% |
| erc721 | 3074 | 4759 | 4837 | 7188 | 8438 | 5055 | -35.4% |
| erc721_full | 5639 | 8611 | 8739 | 11869 | 14090 | 8421 | -33.0% |
| exchange | 417 | 614 | 615 | 822 | 822 | 850 | -32.1% |
| executor | 807 | 1439 | 1440 | 2026 | 2026 | 1798 | -43.9% |
| fixed_point | 2831 | 3526 | 3553 | 4285 | 4740 | 3683 | -19.7% |
| flash_borrower | 822 | 1013 | 1056 | 1446 | 1446 | 1225 | -18.9% |
| layout | 705 | 1093 | 1094 | 1308 | 1668 | 1447 | -35.5% |
| lists | 1624 | 2357 | 2352 | 2785 | 3175 | 2698 | -31.0% |
| logs | 277 | 369 | 371 | 565 | 565 | 529 | -24.9% |
| loops | 871 | 1392 | 1401 | 1717 | 2017 | 1759 | -37.4% |
| math | 424 | 672 | 672 | 809 | 989 | 1063 | -36.9% |
| names | 1312 | 2743 | 2805 | 3011 | 3708 | 2869 | -52.2% |
| numerics | 1679 | 3176 | 3185 | 4195 | 4915 | 3764 | -47.1% |
| overloads | 458 | 715 | 723 | 849 | 1089 | 906 | -35.9% |
| ownable_roles | 952 | 1285 | 1296 | 1695 | 2115 | 1455 | -25.9% |
| profile | 586 | 1135 | 1162 | 1515 | 1515 | 866 | -32.3% |
| reasons | 844 | 1312 | 1312 | 1884 | 2064 | 1808 | -35.7% |
| receiver | 454 | 691 | 737 | 1094 | 1094 | 885 | -34.3% |
| receiver1155 | 926 | 1611 | 1770 | 2372 | 2372 | 1806 | -42.5% |
| records | 1520 | 3398 | 3455 | 4489 | 5407 | 4127 | -55.3% |
| registry | 1034 | 2190 | 2240 | 2533 | 3200 | 1699 | -39.1% |
| relay | 1886 | 3081 | 3147 | 3494 | 4040 | 2800 | -32.6% |
| safe_erc20 | 543 | 655 | 655 | 955 | 955 | 757 | -17.1% |
| selectors | 399 | 620 | 622 | 1214 | 1214 | 769 | -35.6% |
| shapes | 1306 | 1790 | 1800 | 2474 | 2684 | 2227 | -27.0% |
| solady_erc1155 | 2707 | 3429 | 3474 | 3765 | 4365 | 4568 | -21.1% |
| solady_erc20 | 1395 | 2083 | 2109 | 2388 | 2808 | 2317 | -33.0% |
| solady_erc721 | 2110 | 2717 | 2745 | 3465 | 4039 | 3138 | -22.3% |
| staking | 2486 | 3256 | 3263 | 4118 | 4838 | 3352 | -23.6% |
| strings | 1724 | 2354 | 2389 | 2890 | 3254 | 2362 | -26.8% |
| tally | 439 | 599 | 641 | 823 | 823 | 618 | -26.7% |
| texts | 4105 | 6477 | 6633 | 7092 | 8666 | 6431 | -36.2% |
| token | 357 | 566 | 571 | 834 | 834 | 765 | -36.9% |
| traits | 468 | 647 | 651 | 979 | 979 | 905 | -27.7% |
| uniswap_v2_factory | 6038 | 9707 | 9787 | 12359 | 13747 | 10120 | -37.8% |
| uniswap_v2_pair | 5073 | 7680 | 7713 | 9634 | 10782 | 8300 | -33.9% |
| vault | 387 | 618 | 620 | 951 | 951 | 856 | -37.4% |
| total | 89342 | 130526 | 132496 | 169644 | 193227 | 143122 | -31.6% |
The builds are --via-ir with 1, 200, 10,000, and 1,000,000 optimizer runs, and the legacy pipeline with 200 runs. The high run counts trade size for gas, so they set the bar for the gas comparison below. All are compiled --no-cbor-metadata, so metadata doesn't count against Solidity.
Smaller code means cheaper deployment, and more room under the EIP-170 contract size limit of 24,576 bytes. A contract that still doesn't fit can run modules from shared facets. Deployment gas falls with it: about 19% across the suite, 22% for the ERC-20, 31% for the ERC-721, and 36% for the Uniswap v2 factory. Each contract is set up as it deploys, by a deploy in Lithe and a constructor in Solidity, with the same arguments.
Gas per transaction
Storage reads and writes dominate the cost of most transactions, and both compilers touch the same slots, so what separates them is everything else a transaction runs: dispatch, argument decoding, arithmetic, loops, memory, hashing, and encoding. On that part Lithe spends about a third less gas than the best solc build of each contract, and about 6% less in total execution gas, or 2% leaving out the pairs the Uniswap factory deploys. Of the 1,577 transactions and calls in the suite, Lithe costs less than every solc build on 1,253, the same on 195, and more on 129, most of those by under 2%. The compiler weighs each choice between smaller and faster code by an estimate of how often the code runs, so a hot path can spend a few bytes to save gas on every call.
Some transactions still cost more than their Solidity versions. A transition that defers an external call runs its storage writes first, so when the call then reverts, those writes were paid for. A Solidity contract that makes the call before writing reverts sooner.
The comparison is like for like
Most of the benchmark contracts are real programs, not microbenchmarks:
- erc20: OpenZeppelin's wizard token, burnable, pausable, and ownable, on the upgradeable contracts deployed without a proxy
- erc20_full: the wizard token with every common add-on: roles for minting and pausing, a cap, permits, votes with delegation and a checkpointed history, and flash loans through a borrower contract
- erc721: OpenZeppelin's wizard NFT with the same extensions, plus
safeTransferFromagainst a receiver contract - erc721_full: the wizard NFT with every common add-on: enumeration, URI storage with a base URI, burning, pausing, auto-increment minting, and ERC-2981 royalties
- erc1155: OpenZeppelin's wizard multi-token, burnable, pausable, and ownable, with supply tracking and an updatable URI
- erc4626: OpenZeppelin's
ERC4626vault - erc6909: OpenZeppelin's
ERC6909with its token supply, metadata, and content URI extensions, built from the standard library's modules, whose storage sits at their own ERC-7201 roots, so its slots are left out like Solady's - staking: Synthetix's
StakingRewardson OpenZeppelin'sOwnableandInitializable - uniswap_v2_factory and uniswap_v2_pair: Uniswap v2-core, ported to Solidity 0.8, with the factory creating each pair with
CREATE2. A pair lands at a different address in each build, so the comparison names it by role, and swaps both ways, liquidity, the protocol fee, and a signed permit replay on each - fixed_point, date_time, and base64: Solady's
FixedPointMathLib,DateTimeLib, andBase64, next to Lithe's standard library - deployers and safe_erc20: OpenZeppelin's
ClonesandSafeERC20, and Solady'sSSTORE2andCREATE3 - executor and exchange: calls whose results feed continuations
- records and layout: packed records, fixed lists, and tuples in storage
- solady_erc20, solady_erc721, and solady_erc1155: Solady's hand-written assembly tokens, with EIP-2612 permits and the Permit2 allowance on the ERC-20, and safe transfers against receiver contracts on the others, next to plain Lithe with the same errors, events, and order of checks
The rest each exercise one part of the language: strings, lists, records, logs, loops, ABI encoding, low-level calls, and errors.
Each Solidity version uses OpenZeppelin where the original does. The benchmark deploys every build of every contract to a local chain and replays the same transactions against each one. It then checks that statuses, return data, logs, and every storage slot agree across all six builds. Lithe lays out storage exactly as Solidity does, so the raw slots have to match byte for byte. Solady's tokens are the exception: they put each value at a hashed slot of their own, which no Solidity layout produces, so their slots are left out and everything else is still compared.
Reproducing
The benchmark needs Foundry (anvil and cast) and solc. Set SOLC to pick a specific binary. From the repository root:
cargo build --release
python3 bench/run.py # every contract
python3 bench/run.py erc20 # one contractThe Lithe and Solidity sources are side by side in bench/: erc20.lithe next to erc20.sol, and so on. A contract answers ERC-165 only where it implements it, in both languages: erc721, erc1155, erc6909, and erc20_full list their standards in supports, and erc721_full writes its own supportsInterface to add ERC-4906.
These numbers come from the compiler version this site was built with. Rerun the benchmark for current figures.

