Abstract

A contract size limit of 24KB was introduced by EIP #170 to solve the problem:

“When a contract is called, even though the call takes a constant amount of gas, the call can trigger O(n) cost in terms of reading the code from disk, preprocessing the code for VM execution, and also adding O(n) data to the Merkle proof for the block’s proof-of-validity”.

I think we can solve this problem in a way that allows contract size to be higher than 24KB. Complex dApps require complex smart contracts. I have seen many dApp developers struggle with this. A few people have provided their feedback on this GitHub Issue, and I know many more that are struggling with this. When this limit was introduced, It was not a big Issue, but since then, many things have changed like:

More complex dApps are now being built on top of Ethereum, and after Serenity upgrade is complete, Ethereum will be able to support even more complex dApps. EIP 838 Allows adding reason strings to reverts which makes it much easier to debug smart contracts. However, the reason strings take up a lot of contract size. Although the EIP is not finalised, most clients already support this, and it is actively used. Newer versions of Solidity have more type checks that Increased the size of contracts. Often, you’ll find that a contract that was working fine with an old version of solidity is no longer deployable with newer versions of Solidity.

Current Solution

Delegate call proxy patterns and Libraries: Using delegate calls, you can store the code of your smart contract in parts in different contracts and have a “dispatcher” contract that calls the actual contract using delegate calls. Please refer to ZeppelinOS’s upgradability contracts and EIP #1538. If you are interested in understanding how this works, I wrote a blog post that you can read. Libraries also use delegate call under the hood. If you want to learn more about Libraries, I recommend this blog post by Aragon.

Limitations of the current solution

It adds extra unnecessary code. Proxy patterns add another attack vector. Proxy patterns make calls to contracts a lot more expensive as the Proxy has to copy the parameters make an external delegate call every time. Proxy patterns like EIP 1538 also make inter-logic contract call a lot more expensive. We have to make an expensive external call to the proxy that in turns make a similar call to the other logic if we had to access a function that is in the other logic contract. If the contract size was larger, both these functions could have been in the same contract, and we would have had to make only an internal call which is almost free. Proxy patterns hurt readability for the end user. The actual contract code/ABI of the proxy is different from the one we need to access the contract behind the proxy. Like it or not but most people can barely use the read/write features in etherscan, they can’t load a custom ABI and make web3 calls. Proxies make smart contract development slightly more complex. Proxies make it harder to verify the actual code and hence reduce trust. Loading a large contract will be sequential read while loading multiple small ones will be random read. Sequential read is way faster than random reads.

Possible Solutions

Increase contract size limit to 32,768 (2**15, also happens to be exactly the size of two 16k I/O blocks) as it was proposed by @gavofyork in the EIP 170 discussion. This change will give dApp developers a 50% more code size to work with which will be able to accommodate the reason strings and other Solidity changes. This is the easiest solution to implement. If required, relevant opcodes like CALL can have their cost increased by a bit through EIP #1838. Allow infinite contract size and make the cost of OPCODES like CALL dynamic to allow for this change. Code size will still be limited by block gas limit. This is a moderate difficulty change but makes a lot of sense IMO. More on this later. Allow infinite contract size by Implementing paging of contract code as suggested by @SergioDemianLerner in EIP #170 discussion: Make contracts a vector of 24 Kbytes pages. The first page is loaded for free when a CALL is received while jumping or running into another page pays a fee (500 gas) because the page must be fetched from disk. This is an interesting approach but as Vitalik said, “In the long term, we could do pagination, but doing that properly would require changing the hash algorithm used to store contract code - specifically, making it a Patricia tree rather than a simple sha3 hash - and that would increase protocol complexity, so I’m not sure that would actually create significant improvements on top of the delegatecall approach.”

Making the cost of OPCODES like CALL dynamic

Method

The ethereum account array in state trie saves another element codeSize apart from existing 4: [nonce,balance,storageRoot,codeHash] Whenever a new contract is deployed, its size is stored in codeSize of the account object. If a contract is destructed, the codeSize should also be reset. opcodes like CALL, DELEGATECALL, CALLCODE etc should charge additional X(3?) gas per extra word if the contract code size is greater than 24KB.

Rationale

The only reason why the contract size was limited was to prevent people from exploiting the fixed costs. We can overcome this by making the costs variable. The codeSize element will help in calculating call cost before reading the whole contract and eventually throwing OOG. Merkle proofs will also be generated at a fixed cost as we won’t have to load whole contracts from disk first. The codeSize should be enough for generating Merkle proofs of calls that are going to be OOG due to contract size.

Backwards Compatibility

All the existing accounts have less than 24KB of code, so no extra cost has to be charged from calls being sent to them. We can assume words to charge extra gas for = 0 if it’s not available. Alternatively, we can refactor existing DB to include proper codeSize of every contract so that we can use this variable in other things. The hashes before FORK_BLOCK will be generated as if there was no such field and hashes after the FORK_BLOCK will contain this field.