Solidity Smart Contract Bytecode Optimization

Techy Times
3 min readAug 31, 2023

--

The maximum limit of a Solidity smart contract bytecode size is between 24 kb — 25 kb. A contract featuring bytecode beyond this cannot be deployed to the Ethereum mainnet. From the official docs:

“On November 22, 2016 the Spurious Dragon hard-fork introduced EIP-170 which added a smart contract size limit of 24.576 kb.”

Let us take a depper look to understand better why we even have such limits, and what to do to keep contract bytecode size under 24K bytes.

Ethereum Blockchain: Optimizing Smart Contract Bycode Size

Why Have a Bytecode Limit?

To prevent DoS (Denial-of-Service) attacks, there is a limit to the max. size of the contract (the size of the deployment bytecode) which can be deployed to the blockchain.

The Reason?

When the contract size increases, caller’s gas usage does not increase much, as they have to pay only as much gas as required for the function execution, but miner’s resource usage to run a function increases, as they have to read a bigger contract into RAM now. Attackers can, therefore, create a really huge contract and then call a tiny function in there. This would cost attackers less than a dollar, perhaps, but miner’s RAM may get chocked up reading such a huge contract, hanging their system. Given that all the miners run each transaction, each one of them will have their computers frozen, and the network service will go down for all. That’s why it is crucial to have a contract size limit.

How to Save on Bytecode Size?

To decrease the bytes size of your contracts, follow the following tips (labelled either as MEDIUM or LARGE based on the impact they have on the contract bytecode size reduction):

  • [ MEDIUM ]: Combine Functions: Rather than creating multiple functions, combine the code into a single function
  • [ MEDIUM ]: In-line Function Calls: Rather than creating a function, do processing of that function in the same caller function. Ditch the other function.
  • [ MEDIUM ] Create Less In-Memory Variables: Don’t create a lot of variables inside the functions in memory. Rather, use the incoming parameters straightaway from calldata
  • [ MEDIUM ]: Custom Errors & Small Error Strings: Small error strings and custom errors (error Unauthorized(); for example) should be used rather than long strings. From Solidity docs:

“Custom errors have been introduced in Solidity 0.8.4. They are a great way to reduce the size of your contracts, because they are ABI-encoded as selectors (just like functions are)”

  • [ LARGE ]: Use Compiler Optimizations: Compiler optimization can do a LOT of things, like removing unused code, reordering function calls, in-lining functions, combining functions, etc. In Hardhat config file — in case the reader is familiar with the Hardhat ecosystem and works with it — add this code to enable compiler optimization during compiling:
solidity: {
compilers: [
{
version: "0.8.21",
settings: {
optimizer: {
enabled: SHOULD_ENABLE_OPTIMIZATION, // true or false
runs: OPTIMIZER_RUNS,
/*
Runs:
Default: 200
Min: 1
Max: 4.2 billion = (2 ^ 32) - 1

Runs determine for how many function calls a
contract function should be optimized. According
to the docs:
> 01. Smaller value means cheap deployment but expensive function calls
> 02. Larger value means expensive deployment but cheaper function
*/
},
},
},
],
},

Read the official Solidity docs to understand the runs parameter and its impact on smart contract optimization better. Now, when you compile your contracts with:

npx hardhat compile

they will be optimized during compilation.

  • [ LARGE ]: Separate Contracts: Split your contracts into multiple contracts. This has a huge effect in decreasing a single contract’s bytecode size
  • [ LARGE ]: More Library Usage: From Ethereum docs:

“Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the DELEGATECALL.”

Use libraries, rather than writing everything on your own. Libraries use “delegateCall” to preserve context and run external code to modify the caller’s contract state. So, when library functions are not internal (because internal members are inherited by children!), they can save on bytecode size a lot. If they are internal, however, they will all be added to the base contract, and, therefore, increase its size. OpenZeppelin, for example, has almost all functions in several of its libraries marked as “internal.” Hence, they are added to the base contract, increasing our contract’s bytecode size.

Until next time!

--

--

Techy Times

Your monthly lessons on software development, AI, blockchains, cloud computing and, yes, system design and architecture.