Using libraries in Solidity to save on Bytecode size

I am trying to prove that using libraries can save contract bytecode. In this way, I am comparing bytecode using a library (with internal modifiers) and a library (with external modifiers). The thing is that the latter way results in a bigger bytecode size. Am I missing something?

I am not so familiar with the bytecode management by the EVM. is there any resource about it?
I know that library bytecode and address is added to the bytecode but, how the EVM manage this? Why is needed to add the lib’s bytecode if it is already deployed? for integrity sake?

Thanks in advance,

PS: If needed I can upload some code snippets

1 Like

Hi @miguelmtzinf,

Thanks for posting in the forum.

I would have expected a contract calling a library and linking to it would have smaller bytecode size based on https://docs.soliditylang.org/en/v0.7.5/using-the-compiler.html?highlight=library#library-linking

I would have expected a contract calling a library, using only internal functions would be inlined so the bytecode size would increase.

calls to internal functions use the internal calling convention, which means that all internal types can be passed and types stored in memory will be passed by reference and not copied. To realize this in the EVM, code of internal library functions and all functions called from therein will at compile time be included in the calling contract, and a regular JUMP call will be used instead of a DELEGATECALL .
From: https://solidity.readthedocs.io/en/latest/contracts.html#libraries


I tried to create a simple example but it didn’t match my expectation. A more realistic example may be required.

Can you share what you have used?

A.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

library A {
    function doStuff() public returns (uint256) {
        return 42;
    }
}

B.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "./A.sol";

contract B {

    event Stuff(uint256 value);

    function doStuff() public {
        uint256 value = A.doStuff();
        emit Stuff(value);
    }
}

C.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

library C {
    function doStuff() internal returns (uint256) {
        return 42;
    }
}

D.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "./C.sol";

contract D {

    event Stuff(uint256 value);

    function doStuff() public {
        uint256 value = C.doStuff();
        emit Stuff(value);
    }
}

2_deploy.js

// migrations/2_deploy.js
const A = artifacts.require("A");
const B = artifacts.require("B");
const D = artifacts.require("D");

module.exports = async function (deployer) {
  await deployer.deploy(A);
  await deployer.link(A, B);
  await deployer.deploy(B);

  await deployer.deploy(D);
};

Bytecode size

I expected that D would have the larger byte code size.

$ jq '.deployedBytecode | length / 2 - 1' build/contracts/B.json
293
$ jq '.deployedBytecode | length / 2 - 1' build/contracts/D.json
184
1 Like

Hi @abcoathup and thanks for your reply.

That’s the problem itself, the practical side does not match the expectation / theory. I could share some code snippets but It is almost the same example.

After reading docs and chatting with some colleagues I came to the conclusion that using external calls for libraries is worth only when the library’s code is complex enough. DELETEGATECALL opcode for external call takes some bytecode and gas and it could be more expensive than CALL opcode.

As far as I saw, it is more widespread (commonly) to keep the calls to libraries as internal since it is saves more bytecode and gas. (Using internal calls to libraries makes the library code being included in the contract caller as a contract base)

Having said that, It is also important to think about deployment vs code execution costs to decide whether to use internal/external calls to libraries.

I would like to get some words from OpenZepellin security team since I am sure they have some great insights about it.

1 Like

Hi @miguelmtzinf,

I suspect that I needed to use a more complex example.

The libraries in OpenZeppelin Contracts have internal functions and are hence inlined.

I am not sure how much use there is of external libraries.

The library bytecode is not embedded in a contract that uses the library externally. There is no kind of integrity check done before the delegatecall.

To see the gas improvements you need to look at more complex examples. I put together the following silly example that results in the following bytecode sizes:

  • internal: 391 bytes
  • external: 242 bytes
// SPDX-License-Identifier: MIT
pragma solidity 0.7.5;

struct Data {
    uint x;
    mapping (uint => uint) m;
}

library Foo {
    function foo(Data storage data) internal { // change to external to compare
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
        data.x += 1;
        data.m[data.x] += 1;
    }
}

contract Bar {
    Data data;
    
    function test() external {
        Foo.foo(data);
    }
}
2 Likes