How to Save Gas in Smart Contract Systems by Converting External Function Calls to Internal Function Calls
Gas costs increase linearly as more external function calls are made to different smart contracts within a single transaction. External calls are expensive. At the time of this writing an external call costs about 2600 gas. External calls also sometimes require some kind of authentication which requires reading state variables which is also expensive.
A way to get out of this linear increase in gas costs when you want to make multiple external calls to different contracts is to convert the external calls to internal calls. Internal calls are extremely cheap. They are a jump from one place in the code to another.
It is not possible in all cases to convert external calls to internal calls. You can only do it with smart contracts under your control. So this is useful if you are building a larger multi-contract system like an exchange, an NFT platform or DeFi protocol etc.
The first step is to use the diamond pattern as given in EIP2535 Diamonds. I suggest using a diamond reference implementation or other implementation such as from solidstate-solidity or hardhat-deploy.
Let’s say that you have multiple contracts that make external function calls between each other. These contracts become facets of your diamond. A facet is a smart contract whose external functions get added to a diamond.
There are two ways to convert external functions to internal functions. Which way you choose is up to your coding style. But the basic principle is the same: change external functions to internal functions and import them into facets that need them and use them.
Using internal functions of Solidity libraries
One way is to move your external functions that get called between facets to a Solidity library. So move all your external functions in all facets that get called between facets to one Solidity library. Next change them from being external to internal. After that import these internal functions from your Solidity library into the facets that need them and use them. You did it.
Functionality can be written once in internal functions of Solidity libraries and imported into any number of facets that are added to diamonds. Only the actual internal functions used in a particular facet are added to the facet’s bytecode. This makes it possible to write large Solidity libraries without bloating the size of facets that import them.
It is important to understand that internal functions in Solidity libraries work very differently than external functions in Solidity libraries. A Solidity library with external functions is deployed separately from any contracts that use it. Internal functions in Solidity libraries are not deployed separately, they are added to the bytecode of the contracts that use them. So a Solidity library with no external functions is not deployed separately at all.
Using internal functions of contracts that are inherited
The other way to share internal functions among facets used in diamonds is to write contracts that contain only internal functions and inherit them in facets that are added to diamonds. The SolidState Solidity smart contract library uses this approach.
Why it works
This all works because facet functions added to a diamond read and write to the diamond proxy’s contract storage — not to their own.
Sharing internal functions between facets does duplicate the same bytecode among them, which will cost gas to deploy. However it is usually better to optimize for run-time gas costs rather than deployment gas cost.
Also, it should be known that facets can be written in a way that they are reusable after deployment. They can be reused on-chain by different diamonds.
More Reading
More information about how to mitigate or save gas with diamonds and an example of converting external functions to internal functions can be found in this article: