Nowadays, more and more apps and instruments utilize blockchain technology and Ethereum smart contracts. The more complex the business logic behind the smart contract is, the more gas it takes to deploy it to the network. This is due to the fact that when the size of the source code increases, the size of the contract’s bytecode increases as well. Uploading a large amount of bytecode requires a lot of gas. At this time, there is a limitation of about 8 million gas limit per block on the Ethereum blockchain. It means that the total amount of gas for all the transactions in a block including contract deploy cannot exceed this number.

This article will describe several methods that could be used to place large smart contracts on the Ethereum blockchain.

What is a bytecode

Smart contracts cannot function without the Ethereum Virtual Machine. EVM is a runtime environment for executing smart contracts based on Ethereum. The Virtual Machine does not work directly with the Solidity code, it works with the bytecode. The bytecode is a set of instructions for the Virtual Machine, that has strict technical specification described in the Ethereum Yellow Paper . Any smart contract written in Solidity (or any other language for the EVM) is first compiled to the bytecode that will be executed by the Virtual Machine later on.

Let us take a look at a small contract and its bytecode:

contract Example {

mapping(uint => bool) public map;



function setValue(uint key, bool value) {

map[key] = value;

}

}

Bytecode of the Example contract

As you might notice, a large amount of bytecode is generated even for a couple lines of a simple code. An issue stated above (block gas limit exceeded) can take place when your contracts become more complex or start to use other contracts.

Let us have a look at some methods that could be of help here.

Libraries

Code reuse through libraries

A Library is a special type of contract with some limitations. For example, a library cannot receive Ether and does not have its own storage.

Despite some limitations, a library can be used to reduce the bytecode size of the main contract. A library is uploaded to the Ethereum network just once, and then it can be used by several contracts that require this functionality. Thus, you can move various functions to the library in order to reuse the code efficiently. In so doing, you reduce error probability as well as reduce the size of bytecode in the contract that calls the code. This result achieves due to call low-level function which will be described below.

Using signatures to call functions

Low-level function: call

In addition to common language structures, Solidity supports low-level commands as well. One of such commands is call that is used to send message calls to other contracts. An advantage of this approach is that a smart contract does not have to store the bytecode of the called function. This approach can prove to be especially relevant if the bytecode of the called method is too big. As a result, the compiled smart contract will not have any extra opcodes, that in turn will reduce its size. However, please notice that it is not possible to get the return value of the function when the call command is used.

The syntax of the call function looks as follows:

address.call(bytes4(keccak256(<function signature>)), arguments);

In other words, the first argument should be the first 4 bytes of the hash of the called function signature. It is crucial to consider the following when writing the function signature:

For any size of the uint type (uint8, uint16, etc.) it is necessary to use uint256 in the signature; When being listed, the arguments should be separated by commas, but with no spaces! Transmission of dynamically sized types, for instance strings or arrays, will not function correctly;

For example, if a contract has the function function test(uint a, uint b) , the correct call of such a function will look as follows: address.call(bytes4(keccak256(“test(uint256,uint256)”)), a, b);

In case the test executing was successful, you will get true , but if an error occurred you will get false . So don’t forget to use require for handling error purposes.

Using interfaces

The same result can be achieved by means of interfaces.

An Interface is a special type of contract, limited to what the Contract ABI can represent. Put it in other way, it is possible to describe the function signature in the interface, but not the implementation. Using interfaces is convenient for a couple of reasons:

The code readability improves, as compared to the explicit usage of call ; You can handle the return value;

The use of an interface can be shown with the following example:

interface ICallee {

function makeCall(uint val) returns(uint);

}



contract Caller {

ICallee public callee;



function call() {

callee.makeCall(42);

}

}

The Caller contract knows the interface of the ICallee contract, that is why its bytecode will have hash of signature of the makeCall function: …16637e839a3c602a…

So, it becomes possible to use a function from another contract without storing its bytecode in the called contract.

Code decomposition

Code decomposition through libraries

Code reuse is not the sole purpose of a library. For instance, in Daox smart contracts ecosystem CrowdsaleDAO Factory uses the library to split bytecode into two contracts. To avoid uploading the bytecode of the CrowdsaleDAO contract along with the bytecode of the CrowdsaleDAO Factory contract we moved the process of its creation into a separate light-weighted DAODeployer library, which is uploaded independently from the factory:

contract CrowdsaleDAOFactory is DAOFactoryInterface {

function createCrowdsaleDAO(string _name, string _description) public {

address dao = DAODeployer.deployCrowdsaleDAO(_name, _description);

DAODeployer.transferOwnership(dao, msg.sender);

}

}



library DAODeployer {

function deployCrowdsaleDAO(string _name, string _description)

returns(CrowdsaleDAO dao) {

dao = new CrowdsaleDAO(_name, _description);

}



function transferOwnership(address _dao, address _newOwner) {

CrowdsaleDAO(_dao).transferOwnership(_newOwner);

}

}

The following two images show how effective this method is: