Solidity Gotchas — Part 3: Compile Size Limitations and Libraries

Simon Palmer
5 min readApr 21, 2022

--

I guess I should have done my research before pretending I was a Solidity developer, but I didn’t. How hard can it be, right?

If I had I would have been prepared for the monster refactoring that I hit after about a month of development when the evil

Warning: Contract code size is 26783 bytes and exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.

A Spurious Dragon indeed.

What does this mean?

Put simply, your contract is too big and you need to split it up.

When you get this error I confidently predict you will…

  • Monkey around with compiler settings
  • Investigate diamonds
  • Make all your error messages really small
  • Then refactor your code

Why is there a limit?

I mean, WT Actual F. A Limit to how much delicious code I can write?

Actually there is a good reason and that is to avoid DOS attacks on the Ethereum network. The cost to the whole network of a contract call is proportional to the size of the contract. If I wanted to be evil, I would write a giant contract the size of the Smithsonian Library and then call it endlessly and render the network useless.

During a hard fork in 2016, EIP-170 was introduced which capped the contract size limit. The Spurious Dragon was unleashed.

What can I do about it?

The smart move is to step back and look at your whole domain model. Ask yourself…

  • why is that one contract SO large?
  • why does that entity contain all those operations (and probably data)?
  • Can I compose my domain model differently?
  • Can I split my code differently?

The answer to the last question is where Libraries play.

What’s a library?

It’s exactly what you think it is; a location where your code resides. But it’s not a contract. The EVM itself doesn’t make a huge distinction, but it enables you to factor out the code from your contract to reduce its size.

When you interact with the code in your library it’s not dissimilar to a delegate call from the EVM’s standpoint.

You declare one like this…

Library MyContractCode {
function _multiply(uint number, uint multiplier)
pure
returns (uint){
return number * multiplier;
}
}

To call the code you include a reference to the library in your contract and you can then get at its functions using Library.function.

import './MyContractCode.sol';Contract MyContract{
uint myNumber;
uint myMult;
function multiply() returns (uint){
return MyContractCode._multiply(myNumber, myMult);
}
}

If you are careful you can factor your contract’s code out almost entirely. Don’t be surprised if your contract starts to look like an empty shell containing only state data and the barest minimum of validation.

This has the added advantage of making your functional testing easier because, done right, you could be passing in all state on which your functions operate.

This pattern has much to be said for it as it also starts to open the door to upgradeability of your contracts. If you aren’t worried about that, then you haven’t thought about it enough and you need to go and worry about it.

If you are thinking that you have the elegant possibility of making a suite of purely functional methods (virtual in other worlds), then watch out. The Dragon is Spurious.

Pure and inlining

This is a bit of a gotcha for libraries, especially if your first motivation is reducing contract size. If you make a library function pure, then the compiler will lift the code and insert it into your original contract at compile time. As a result your contract size grows and you are back with the same problem you had before.

As a result it’s a reasonable default practice to make your library functions public. Doing this means they don’t get inlined with your contract code.

Instead of the declaration for our library above, change it like this…

Library MyContractCode {
function _multiply(uint number, uint multiplier)
public
returns (uint){
return number * multiplier;
}
}

Now the code is treated like a delegate call in which this is the calling contract. Dragon back in its Spurious cave.

There is another issue you may well bump into if you start to pattern your code with libraries and stateless functions and that is the stack depth. I will cover that in a separate article, but for now, just try not to pass more than a few arguments to each function.

Duplicate declaration of events

If you make use of events and want them in your library then you have to take an additional step. I found this very counterintuitive, but hey, it works.

If you declare an Event in your library like this…

Library MyContractCode {    Event Multiplied(uint number, uint multiplier, uint result);    function _multiply(uint number, uint multiplier) 
public
returns (uint){
uint result = number * multiplier;
emit Multiplied(number, multiplier, result);
return result;
}
}

…then any external code that is consuming your contract’s interface (for instance web3) won’t know about the event because it’s in the library. The event will still get emitted, but you can’t directly subscribe — which is a pain.

To overcome this you need to duplicate the Event definition in the contract.

import './MyContractCode.sol';Contract MyContract{
uint myNumber;
uint myMult;
Event Multiplied(uint number, uint multiplier, uint result); function multiply() returns (uint){
return MyContractCode._multiply(myNumber, myMult);
}
}

Now you can subscribe to it as normal from web3 and the event emitted by the library will be caught by your subscription handler.

Deploying — oops, where has my code gone?

So now you have separated your contract’s state from its code, you need to associate them together whenever your contract gets deployed.

Exactly how you do this in your development environment will depend on what you are using. I am using truffle and ganache, so I will describe that, especially as it turned out to be quite elegant and straightforward once I figured it out.

const factory = artifacts.require("factory");
const client = artifacts.require("client")
const clientrequestlib = artifacts.require("clientrequestlib");
module.exports = function (deployer) {
deployer.deploy(clientrequestlib);
deployer.link(clientrequestlib, [client, factory]);
deployer.deploy(factory);
};

In my project I have the added complexity that I am using the factory pattern. have a factory contract responsible for creating client contracts. I factored the code of the client contracts out into several libraries, but in the case above I bundled all the code associated with requests between client contracts into a single library clientrequestlib.

The library still needs to be deployed like any other contract (remember, from the EVM’s perspective there’s not a lot of difference between a contract and a library). The deployer.link line above is where the magic happens.

This deployment code goes into a *_migrate.js file in the truffle migrations folder and I deploy using

truffle migrate --reset

Libraries are a huge topic, but that’s it for now. I will add to this as I discover more.

Here are the links to the other articles in this series…

Back to the head article
Part 1: Maps and Arrays
Part 2: Storage, Memory and Calldata
Part 3: Compile size limitations and Libraries
Part 4: Call Stack Depth

Photo by Jon Callow on Unsplash

--

--

Simon Palmer

CTO in the wild. I’ve been in the software industry for approximately forever. Let me know if you need help, especially if you are grappling with blockchain.