2

I was checking into EIP170 since while developing some contracts upon ERC1155 I came across the contract max size restriction of 0x6000 = 24576 bytes. As I later checked, the size of the artifacts files on my build truffle folder differ from what "truffle run contract-size" returns as size for each contract (though they are correlated up to a certain degree).

My question is: which contract features determine the size of it? Which are most relevant at increasing its size? For instance, I was thinking at the number of functions, the number and type of function inputs, the presence of loops inside functions, the number and type of storage variables... But I'm not so sure about how each one of them affects the size of the contract.

Thanks in advance to all of you.

1 Answer 1

4

Here are some things that can make your compiled bytecode bigger (roughly from highest to lowest impact):

  1. Using new to deploy other contracts. This one can balloon the size of your contract very quickly even if it's not doing much by itself. See for example my answer for Running new() inside my contract adds 20K to contract size?
  2. Compiling without the optimizer enabled. I won't go into how specific optimizations affect the size but you should be using the optimizer if you care about contract size and gas usage. This is especially important with the upcoming Yul-based code generator (which is still experimental and not the default yet) because it's written with the assumption that people are optimizing their code. It emits simpler and more auditable intermediate but potentially more verbose intermediate code and leaves streamlining things and removing unused stuff completely up to the optimizer.
  3. Inheriting from other contracts. If you inherit from a contract, all its external and public functions will also be present in your contract's bytecode. Also, any inherited constructors and internal functions you call. And private functions if they're called by any of these other functions. Only unused internal and private functions can be removed by the optimizer.
  4. Calling internal library functions. internal library functions when called from a contract, become a part of that contract's bytecode while external and public library functions are only a part of the library. External calls are more expensive than internal ones but if you have N contracts calling the same library function it might still overall be cheaper to make it an external call.
  5. Invoking modifiers. Currently modifiers are always inlined into the body of the function so each use takes the amount of space proportional to the size of the implementation (unless, of course, the optimizer can reduce it). This is going to change soon but only in the new code generator so it's still relevant and will stay relevant for older compiler versions.
  6. Using a high value of for --optimize-runs. High values tell the optimizer that your functions will be called often and it's more important to you how much gas they use at runtime than how expensive it is to deploy the contract. If there's a tradeoff between size and gas usage, the optimizer will be the more inclined to forgive the size increase the higher this value is.
  7. Anything that indirectly uses the abicoder. Things like accessing parameters in external functions, passing parameters to other external functions, emitting events and custom errors. Especially when these parameters are structs or arrays. The impact of this is not that big and it's often code that you would have to write yourself otherwise but it's still something to keep in mind - values passed around in external calls need special encoding and the compiler inserts extra code for encoding and decoding them.
  8. Checked arithmetic. Checking for overflows in arithmetic operations obviously requires more code than not checking them. Ideally, the optimizer would remove these checks in places where they're superfluous but unfortunately this does not happen in all cases.
  9. Defining public state variables. The compiler automatically generates small external functions for accessing their values.
  10. Defining external and public contract functions. External functions cannot be removed by the optimizer even if they are unused so if a function is not meant to be ever called from the outside of your contract, making it public rather than internal or private is a bit of a waste (and might be a security issue too). I'm putting it last because you're unlikely to find such functions in a well designed contract. If it's not well designed, you could potentially save a lot though.

This is what I can list off the top of my head, I hope I'm not forgetting anything big.

Also note that the limit from EIP-170 refers to the deployed code, which means that anything you do in the constructors or functions used to initialize state variables does not count against that limit.

In general if you are worried about the size of your contract, it might be a good idea to look at the IR output of the compiler, i.e. solc --optimize --ir-optimized. Keep in mind that it's likely not exactly what you're getting currently since the compiler still uses the legacy, non-Yul code generator by default, but the output is functionally equivalent and Yul should be readable enough for you to spot what's taking up the most space. Especially if you've ever used inline assembly in Solidity.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.