A sideBySide study of seven Solidity bytecodeReduction techniques applied to a multiSignature escrow contract with milestone support. Original: 939 lines, 24.186 KiB deployed bytecode (
EscrowBefore.sol). Optimized: 534 lines, 9.697 KiB deployed bytecode (EscrowAfter.sol). 60% smaller, with no loss of functionality and identical test pass rate.
Stack · Solidity 0.8.24 · Hardhat · hardhatContractSizer · solidityCoverage · OpenZeppelin
Domain · Solidity gas optimization · EVM bytecode · Compiler flags · Custom errors · Storage packing
Status · StandAlone study repository
flowchart LR
A[EscrowBefore.sol<br/>939 lines · 24.186 KiB bytecode<br/>72 require strings · address admin1+admin2<br/>9-state status enum · separate milestone struct]
B[7 systematic optimizations]
C[EscrowAfter.sol<br/>534 lines · 9.697 KiB bytecode<br/>2-3 char custom errors · address admins 2<br/>6-state status · unified milestone embed]
A --> B --> C
style A fill:#fee,stroke:#c33
style C fill:#efe,stroke:#3c3
- Subject · A 939-line multiSignature escrow contract supporting both singleToken and milestoneBased escrows, native ETH and ERC20 transfers, platform fee collection, dispute resolution with 2-of2 admin signatures, and dualAdmin architecture. Functional but bytecodeHeavy at 24.186 KiB — close to the 24.576 KiB EIP170 contractSize limit.
- Goal · Reduce deployed bytecode below half the original while preserving the test suite's behavioral guarantees. The driving constraint was the Spurious Dragon contractSize limit; the contract was within ~400 bytes of being undeployable.
- Result · 24.186 KiB → 9.697 KiB. 60% reduction. Initcode shrunk from 24.554 KiB → 10.283 KiB (58% reduction). All 30 Hardhat tests pass against both contracts.
- Approach · Seven systematic techniques applied in order — see techStack.md. Order matters: storage packing first (changes the type signatures of every function), then function consolidation (uses the nowPacked types), then custom errors (replaces the require strings exposed by the consolidated functions).
Most "Solidity gas optimization" articles are microTips — ++i over i++, packed structs, immutable variables. They help, but they're rarely the leverage point. The leverage point is architectural: removing redundant state, collapsing duplicate function paths, and swapping verbose error reporting for compact error codes.
This study demonstrates that on a real contract under a real constraint (EIP170). Each technique is documented with the specific bytecode delta it contributed, so readers can prioritize their own optimization work by impact.
| Metric | Before | After | Reduction |
|---|---|---|---|
| Source LOC | 939 | 534 | 43% |
| Deployed bytecode | 24,186 bytes | 9,697 bytes | 60% |
| Initcode (deployment cost) | 24,554 bytes | 10,283 bytes | 58% |
require string count |
72 | 0 | 100% |
| Test pass rate | 30/30 | 30/30 | unchanged |
| Solidity version | 0.8.24 | 0.8.24 | unchanged |
| Compiler optimizer runs | 100 | 100 | unchanged |
Source: optimised.jpeg in the original repo (Hardhat contractSizer output).
Each links into techStack.md with What / Why / When / How / Alternatives / Pros / Cons.
- Custom error codes — replaced 72
require(_, "Insufficient token amounts")strings with 2-3 char codes (ITA,ZAF,UP). Each string costs 30+ bytes; each code costs ~6. - Storage layout consolidation —
address admin1; address admin2;→address[2] admins;. Removed redundant counters (totalMilestoneEscrows,totalDisputes,myMilestoneEscrows). - Status enum compression — 9 states → 6 states; consolidated overlapping milestoneAndEscrow states into a unified set.
- Function consolidation — merged
acceptMilestoneEscrow()+acceptEscrow()into one handler; mergedcreateMilestoneEscrow()+createEscrow()viaEscrowTypeenum + array params. - Calldata array params —
address[] memory tokens, uint[] memory amountsinstead of N separate singleAsset variants. - Removed dead code paths — stripped
myMilestoneEscrowsindexing (unused), perEscrow token field redundancy. - Compiler optimizer settings —
runs: 100balances deployment cost vs runtime cost. Higher values inline more aggressively (better runtime, larger bytecode); 100 was empirically optimal for this contract's call mix.
contracts/
├── EscrowBefore.sol # Original (939 lines, 24.186 KiB)
├── EscrowAfter.sol # Optimized (534 lines, 9.697 KiB)
└── errorCodes.txt # The 72 → 2-3 char abbreviation table
docs/
└── flows/
└── beforeAfter.md # SideBySide diffs of the key transformations
architecture.md # Storage layout + function surface comparison
techStack.md # The seven techniques, in detail
deepDive.md # Walking through one optimization endToEnd
Both contracts are published in this repo because the contract is a study artifact — its purpose is to be read.
The Hardhat test suite (756 lines, validates functional parity between
EscrowBeforeandEscrowAfter) is referenced in the docs but not included to keep the study focused on the optimization itself.
Built by Hafiz Abdullah · hafiz.abdullah641@gmail.com · Open to interviews under NDA.