We compared EVM bytecode size, gas usage and execution time of popular mainnet contracts (UniswapV3 and ENS DNSRegistrar). We used Solidity compiler branch which supports most of the EOF features.
For Uniswap contracts we achieved benefits in all measured stats.
- Deployed code and initcode size are
6.5%smaller. - Gas usage for deploy and call step is
~14%and~9%lower. - Gas usage for simple
swapExact0For1is also~5%lower. - Execution time on
evmonefast EVM implementation also looks better- For 100 calls of
swapExact0For1(jump dest analysis enabled for legacy) execution time of EOF bytecode is~15%shorter than legacy. - Benchmarks prepared in a form of state tests (legacy, EOF) (1000
swapExact0For1calls) also shows measurable benefits from using EOF:- EOF:
35ms(~10-15% faster than legacy w/o jump dest analysis) - Legacy:
39ms - Legacy with jump dest analysis:
68ms
- EOF:
- For 100 calls of
For DNSRegistrar contract test case we also achieved benefits in all metricses.
- Deploy code and initcode size for the test case are accordingly
~6%and~1.5%smaller. - Gas usage for
proveAndClaimfunction call is~10%lower. - Also generated by
solccompiler bytecode size ofDNSRegistrarcontract is~10%smaller.
Command:
legacy
ETH_EVMONE=<PATH_TO_EVMONE_BUILD>/libevmone.dylib ./build/test/./soltest -t "semanticTests/UniswapV3Flattened" -- --optimize --evm-version cancun --show-messages
eof
ETH_EVMONE=<PATH_TO_EVMONE_BUILD>/libevmone.dylib ./build/test/./soltest -t "semanticTests/UniswapV3Flattened" -- --optimize --evm-version cancun --eof-version 1 --show-messages
LEGACY:
init code: 31 202 bytes
deployed code: 30 979 bytes
EOF:
init code: 29 236 bytes
deployed code: 29 048 bytes
init code: ~6.5% less
deployed code: ~6.5%less
Gas usage statistics for Uniswap V3 and simple run consists of:
- deploy
UniswapV3Factory - call
runTest- deploy ERC20 tokens
- deploy uniswap pool
- add liquidity
- simple swap
LEGACY:
deploy step: 6 832 734 total gas
call step: 8 815 561 total
EOF:
deploy step: 5 925 377 total gas
call step: 8 094 095 total
deploy step: ~14% less
call step: ~9% less
Simple swap EOF:
gas used: 77389
gas used (without refund): 80189
Simple swap legacy:
gas used: 81329
gas used (without refund): 84129
Execution on evmone rev d53b9e21cf39cec7f52253c141ea28572650a949
EOF: 13870 microseconds
legacy: 18539 microseconds
Machine:
Apple M3 Pro
36GB RAM
evmone commands:
build/bin/./evmone-statetest --trace-summary ./test/evm-benchmarks/benchmarks/main/uniswapv3_many_swaps_legacy.json
build/bin/./evmone-statetest --trace-summary ./test/evm-benchmarks/benchmarks/main/uniswapv3_many_swaps_eof.json
State test jsons: Legacy: https://gist.github.com/rodiazet/28a50d5e67d25e8dd9d87ad5962c9f25 EOF: https://gist.github.com/rodiazet/d9fbdb3f25ca77bb8ede133c1736e318
Summary:
Tx Execution
EOF: 35ms
Legacy: 39ms
Legacy with jump dest analysis: 68ms
The code of this contract is bigger than UniswapV3Factory on mainnet because we added for test purpose runTest function which relies on ERC20 contract code and much more. But when running the test with empty runTest function with, EOF the bytecode is smaller by similar % than the one on mainnet.
Custom soltest:
Source: https://github.com/ipsilon/solidity/tree/ens-test
DNSRegistrar semantic test: https://github.com/ipsilon/solidity/blob/ens-test/test/libsolidity/semanticTests/DNSRegistrarFlattened.sol
LEGACY:
init code: 27 845 bytes
deployed code: 3 415 bytes
EOF:
init code: 26 171 bytes
deployed code: 3 375 bytes
init code: ~6% less
deployed code: ~1.5%less
Gas usage statistics for DNSRegistrar::proveAndClaim method call:
LEGACY:
call proveAndClaim: 251 846 total
EOF:
call proveAndClaim: 228 466 total
call step: ~10% less gas used for EOF
Deployed code size comparison for clean DNSRegistrar contract
- legacy optimized:
7 058bytes - eof optimized:
6 388bytes
Diff ~10%
Compiler flags used:
legacy
--via-ir --bin-runtime --optimize
eof
--via-ir --bin-runtime --experimental-eof-version --optimize