How to Reduce Smart Contract Size
Table of Contents
- Summary
- EIP-170: Ethereum Smart Contract Size Limit
- Quadratic Vulnerability
- How to Check Smart Contract Size
- How to Reduce Smart Contract Size
Summary
EIP-170 introduced a size limit for smart contracts to prevent network congestion and DoS attacks.
To optimize smart contract size, developers should:
- Modularize contracts (split large contracts).
- Use libraries to offload reusable logic.
- Apply custom errors and shorter messages.
- Optimize Solidity compiler settings.
- Reduce function count and variable declarations.
EIP-170: Ethereum Smart Contract Size Limit
EIP-170 was introduced during the Spurious Dragon hard fork on November 22, 2016, to protect the Ethereum network from Denial of Service (DoS) attacks. This proposal limits the size of smart contracts to 24,576 bytes (24.576 KB).
EIP-170 Implementation
If block.number >= FORK_BLKNUM, then if contract creation initialization returns data with length of more than MAX_CODE_SIZE bytes, contract creation fails with an out of gas error.
- MAX_CODE_SIZE: 0x6000 (2**14 + 2**13)
- FORK_BLKNUM: 2,675,000
- CHAIN_ID: 1 (Mainnet)
Quadratic Vulnerability
Ethereum transactions consume gas in two main ways:
Pre-processing a smart contract
- Before execution, the Ethereum Virtual Machine (EVM) loads and processes the contract code (O(n) complexity).
Merkle Proof Validation
- The blockchain validates transactions using Merkle proofs, requiring additional computation (O(n) complexity).
A Quadratic Vulnerability arises when a contract’s size significantly impacts execution performance. Even simple functions can become expensive and slow if the contract size is too large.
How to Check Smart Contract Size
Smart contracts are compiled into bytecode, which determines their final size.
A common tool for checking smart contract sizes is truffle-contract-size.
How to Reduce Smart Contract Size
The Ethereum Developer Guide outlines three key strategies to optimize smart contract size.
- Step 1 (Big Impact): Major optimizations.
- Step 2 (Medium Impact): Code refactoring and simplifications.
- Step 3 (Small Impact): Minor optimizations.
1. Big Impact: Major Contract Optimizations
1.1. Split Large Contracts
Breaking down smart contracts into smaller, modular components improves:
- Readability
- Maintainability
- Performance
Best Practices for Contract Separation
- Group related functions together.
- Separate functions that don’t modify state.
- Decouple storage-heavy operations.
1.2. Use Libraries
Libraries allow reusable code without increasing contract size.
Important Notes:
- Libraries cannot have internal functions (they must be external or public).
- Use Solidity’s using for keyword to apply libraries efficiently.
pragma solidity >=0.6.0 <0.7.0;
struct Data { mapping(uint => bool) flags; }
library Set {
function insert(Data storage self, uint value) public returns (bool) {
if (self.flags[value]) return false; // Already exists
self.flags[value] = true;
return true;
}
}
contract C {
using Set for Data;
Data knownValues;
function register(uint value) public {
require(knownValues.insert(value));
}
}
1.3. Implement Proxies
Proxies use delegatecall to execute logic from another contract, keeping the main contract lightweight.
- Common in upgradable smart contracts (Guide).
- Increases complexity, so use only if absolutely necessary.
2. Medium Impact: Code Refactoring
2.1. Reduce the Number of Functions
- External Functions: Minimize view functions unless necessary.
- Internal Functions: Inline single-use functions instead of defining them separately.
2.2. Reuse Variables
Before Optimization:
function get(uint id) returns (address, address) {
MyStruct memory myStruct = myStructs[id];
return (myStruct.addr1, myStruct.addr2);
}
After Optimization:
function get(uint id) returns (address, address) {
return (myStructs[id].addr1, myStructs[id].addr2);
}
This reduces contract size by 0.28 KB.
2.3. Use Shorter Error Messages
Error messages contribute to contract size. Use error codes instead of full descriptions.
Before Optimization:
require(msg.sender == owner, "Only the owner can call this function");
After Optimization:
require(msg.sender == owner, "OW1");
2.4. Use Custom Errors
Solidity 0.8.4 introduced custom errors, which reduce contract size by encoding errors efficiently.
error Unauthorized();
if (msg.sender != owner) {
revert Unauthorized();
}
2.5. Adjust Solidity Optimizer Settings
Solidity’s optimizer reduces bytecode size based on call frequency. If a contract is rarely called, use low optimizer settings.
- Default value: 200
- Deployment Optimization: Set to 1 (reduces bytecode size but increases gas cost).
- Runtime Optimization: Set to a higher value to optimize gas usage.
3. Small Impact: Minor Optimizations
3.1. Avoid Passing Structs as Parameters
Using structs as function parameters increases contract size. In this example, we can save ~0.1 KB by passing primitive variables instead of structs.
Before Optimization (Structs):
function get(uint id) returns (address, address) {
return _get(myStruct);
}
function _get(MyStruct memory myStruct) private view returns (address, address) {
return (myStruct.addr1, myStruct.addr2);
}
After Optimization (Primitive Variables):
function get(uint id) returns (address, address) {
return _get(myStructs[id].addr1, myStructs[id].addr2);
}
function _get(address addr1, address addr2) private view returns (address, address) {
return (addr1, addr2);
}
3.2. Use Correct Visibility Modifiers
Optimize function visibility:
- Use public if the function is externally called.
- Use private/internal if it is only used within the contract.
3.3. Remove Modifiers
Modifiers increase contract size. Use functions instead.
Before Optimization (Using Modifier):
modifier checkStuff() {}
function doSomething() checkStuff {}
After Optimization (Using Functions):
function checkStuff() private {}
function doSomething() { checkStuff(); }