Skip to content

abdullahAttiq/case-study-gas-optimization

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Solidity Bytecode Reduction Study — 60% smaller deployed contract through systematic optimization

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


Hero

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
Loading

At a glance

  • 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).

Why this exists

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.


Headline numbers

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).


The seven techniques

Each links into techStack.md with What / Why / When / How / Alternatives / Pros / Cons.

  1. 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.
  2. Storage layout consolidationaddress admin1; address admin2;address[2] admins;. Removed redundant counters (totalMilestoneEscrows, totalDisputes, myMilestoneEscrows).
  3. Status enum compression — 9 states → 6 states; consolidated overlapping milestoneAndEscrow states into a unified set.
  4. Function consolidation — merged acceptMilestoneEscrow() + acceptEscrow() into one handler; merged createMilestoneEscrow() + createEscrow() via EscrowType enum + array params.
  5. Calldata array paramsaddress[] memory tokens, uint[] memory amounts instead of N separate singleAsset variants.
  6. Removed dead code paths — stripped myMilestoneEscrows indexing (unused), perEscrow token field redundancy.
  7. Compiler optimizer settingsruns: 100 balances deployment cost vs runtime cost. Higher values inline more aggressively (better runtime, larger bytecode); 100 was empirically optimal for this contract's call mix.

Repository layout

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

A note on source code

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 EscrowBefore and EscrowAfter) 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.

About

Engineering case study: 60% Solidity bytecode reduction (24.186 KiB → 9.697 KiB) via 7 systematic optimization techniques.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors