# Common Techniques for Reducing Contract Size

&#x20;         To demonstrate several strategies for optimizing contract size, start by creating two contracts: `OptimizedCalculator.sol` and `AdvancedCalculator.sol`, using the following structure:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

contract OptimizedCalculator {
    function add(uint256 a, uint256 b) external pure returns (uint256) {
        require(a > 0 && b > 0, "Invalid values");
        return a + b;
    }

    function sub(uint256 a, uint256 b) external pure returns (uint256) {
        require(a > 0 && b > 0, "Invalid values");
        return a - b;
    }

    function mul(uint256 a, uint256 b) external pure returns (uint256) {
        require(a > 0 && b > 0, "Invalid values");
        return a * b;
    }

    function div(uint256 a, uint256 b) external pure returns (uint256) {
        require(a > 0 && b > 0, "Invalid values");
        return a / b;
    }
}

```

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import "./OptimizedCalculator.sol";

contract AdvancedCalculator is OptimizedCalculator {
    function power(uint256 base, uint256 exponent) public pure returns (uint256) {
        require(base > 0 && exponent > 0, "Invalid values");
        return base ** exponent;
    }
}

```

&#x20;         Next, execute the command `npx hardhat size-contracts` once more, and you should see the following output:

<figure><img src="https://1670530031-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNGHmFVO7DlELOLs4PSVs%2Fuploads%2Fagzew4dfXu14D4bTy8vA%2Fimage25.png?alt=media&#x26;token=77d33624-b932-4282-8451-e7f8ea94f8d5" alt=""><figcaption></figcaption></figure>

&#x20;         Observe that the size of `AdvancedCalculator` is larger than that of `OptimizedCalculator`. This increase is due to `AdvancedCalculator` inheriting from `OptimizedCalculator`, meaning it includes all the functionality and code from the parent contract—which directly impacts its overall size.

## Code Abstraction and Modifiers

&#x20;         At this stage, as a smart contract developer, it's a good idea to review your code and identify opportunities for optimization.

&#x20;         One of the first things you'll likely observe in the codebase is the frequent use of `require` statements. Instead of writing `require(a > 0 && b > 0, "Invalid values");` multiple times, a more efficient approach is to abstract this repetitive logic into a modifier, like the example below:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

contract OptimizedCalculator {
    error InvalidInput();

    function add(uint256 a, uint256 b) external pure onlyValidInputs(a, b) returns (uint256) {
        return a + b;
    }

    function sub(uint256 a, uint256 b) external pure onlyValidInputs(a, b) returns (uint256) {
        return a - b;
    }

    function mul(uint256 a, uint256 b) external pure onlyValidInputs(a, b) returns (uint256) {
        return a * b;
    }

    function div(uint256 a, uint256 b) external pure onlyValidInputs(a, b) returns (uint256) {
        return a / b;
    }

    modifier onlyValidInputs(uint256 a, uint256 b) {
        if (a == 0 || b == 0) {
            revert InvalidInput();
        }
        _;
    }
}

```

&#x20;         And for `AdvancedCalculator`:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import "./OptimizedCalculator.sol";

contract AdvancedCalculator is OptimizedCalculator {
    function power(uint256 base, uint256 exponent) 
        public 
        pure 
        onlyValidInputs(base, exponent) 
        returns (uint256) 
    {
        return base ** exponent;
    }
}

```

&#x20;         Take note of how the modifier is used along with the replacement of the `require` statement by a custom error, which is a more gas-efficient approach.

&#x20;         After running the `npx hardhat size-contracts` command again, you should see the updated size output:

<figure><img src="https://1670530031-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNGHmFVO7DlELOLs4PSVs%2Fuploads%2FW9B7bg7DkFk41LIZ3fTu%2Fimage6.png?alt=media&#x26;token=b1c6af99-2f02-4da7-92fb-563b1ee5a4fa" alt=""><figcaption></figcaption></figure>

&#x20;         Even though the size reduction is minor, you can already observe some improvement.\
&#x20;         This optimization process can be repeated until you’re satisfied with the contract’s final size.

## Splitting Into Multiple Contracts

&#x20;         A common practice in smart contract development is to break down larger contracts into smaller, modular ones. This isn’t just useful for staying within size limits—it also improves code clarity, promotes better abstraction, and helps avoid redundancy.

&#x20;         From a size optimization standpoint, splitting a large contract into smaller, standalone ones helps ensure each individual contract remains under the Solidity size limit. For instance, if an initial contract is 30 KiB, dividing it into two separate contracts could yield two contracts of around 15 KiB each—both within acceptable limits. However, it’s important to note that this approach may increase gas costs during execution, as calls to external contracts are more expensive.

&#x20;         To demonstrate this, let’s create a contract named `Car` with a function called `startJourney`:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

contract Car {
    function startJourney() external pure returns (string memory) {
        return "The journey has started!";
    }
}

```

&#x20;         In this example, the `startJourney` function of the `Car` contract depends on specific functionality provided by two separate contracts: `Dashboard` for handling speed display logic and `Engine` for starting the vehicle.

&#x20;         Engine contract:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import "hardhat/console.sol";

contract Engine {
    function ignite() external view {
        console.log("Engine started");
    }
}

```

&#x20;         Dashboard contract:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

contract Dashboard {
    function showSpeed(uint256 speed) external pure returns (uint256) {
        require(speed > 0, "Speed must be greater than 0");
        return speed;
    }
}

```

&#x20;         The simplest way for the `Car` contract to access both the `Dashboard` and `Engine` functionalities would be through inheritance. However, as these contracts continue to grow with added features, the overall size of the compiled code will increase. Eventually, you may hit the contract size limit, since all the inherited logic is copied into the `Car` contract.

&#x20;         A better approach is to keep each functionality within its own dedicated contract. If the `Car` needs to use those features, it can interact with the `Dashboard` and `Engine` contracts via external calls.

&#x20;         In this scenario, the `startJourney` function within the `Car` contract needs to call both the `Dashboard` and `Engine` to complete its operation.

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "./Engine.sol";
import "./Dashboard.sol";

contract Car {
    Engine private engine;
    Dashboard private dashboard;

    constructor(address _engine, address _dashboard) {
        engine = Engine(_engine);
        dashboard = Dashboard(_dashboard);
    }

    function startJourney() external view {
        engine.ignite();
        dashboard.showSpeed(100);
    }
}

```

&#x20;         When you run the contract sizer plugin, you'll see the following output:

<figure><img src="https://1670530031-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNGHmFVO7DlELOLs4PSVs%2Fuploads%2F1HdbN7OcpVrt6eEeZIFr%2Fimage3.png?alt=media&#x26;token=c0e5c757-44f0-4359-9d61-e01e81b32e01" alt=""><figcaption></figcaption></figure>

&#x20;         Observe how the `Car` contract remains relatively small in size, yet it can still access the full functionality provided by both the `Engine` and `Dashboard` contracts.

&#x20;         While this modular approach helps keep each contract within size limits, it's important to note that it may lead to increased gas costs due to external calls—this trade-off is explored in more detail in the Gas Optimization article.

## Leveraging Libraries

&#x20;         Libraries are a widely used method for encapsulating and reusing common logic across multiple smart contracts. They can play a major role in reducing contract size and improving code maintainability. In Solidity, libraries can be categorized as either **internal** or **external**.

&#x20;         Internal libraries function similarly to inherited contracts—when you use them, their code is copied into the final contract during compilation, which increases the contract’s bytecode size.

&#x20;         On the other hand, external libraries behave differently. Solidity interacts with them through a special low-level operation called `delegatecall`, allowing the calling contract to execute the library's code in its own context. Because external libraries are stateless, they behave much like pure functions and can be deployed once and reused by multiple contracts.

&#x20;         In the following example, the `Car` contract will make use of a `Calculator` library only. Here’s how that would look:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

library Calculator {
    error InvalidInput();

    modifier onlyValidInputs(uint256 a, uint256 b) {
        if (a == 0 || b == 0) {
            revert InvalidInput();
        }
        _;
    }

    function add(uint256 a, uint256 b) external pure onlyValidInputs(a, b) returns (uint256) {
        return a + b;
    }

    function sub(uint256 a, uint256 b) external pure onlyValidInputs(a, b) returns (uint256) {
        return a - b;
    }

    function mul(uint256 a, uint256 b) external pure onlyValidInputs(a, b) returns (uint256) {
        return a * b;
    }

    function div(uint256 a, uint256 b) external pure onlyValidInputs(a, b) returns (uint256) {
        return a / b;
    }
}

```

&#x20;         Than Car is:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import "./Engine.sol";
import "./Dashboard.sol";
import "./Calculator.sol";

contract Car {
    using Calculator for uint256;

    Engine private engine;
    Dashboard private dashboard;
    uint256 private speed;

    constructor(address _engine, address _dashboard) {
        engine = Engine(_engine);
        dashboard = Dashboard(_dashboard);
    }

    function startJourney() external {
        engine.ignite();
        uint256 localSpeed = dashboard.showSpeed(100);

        speed = speed.add(localSpeed);
    }

    function getSpeed() external view returns (uint256) {
        return speed;
    }
}

```

&#x20;         Observe how the contract is instructed to use the `Calculator` library for `uint256` types. This allows the `add` function from the `Calculator` library to be used directly on any `uint256` value within the `startJourney` function.

&#x20;         After running the `npx hardhat size-contracts` command, you will see the following output:

<figure><img src="https://1670530031-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNGHmFVO7DlELOLs4PSVs%2Fuploads%2FXG30G69rA2D2O7l28GOY%2Fimage9.png?alt=media&#x26;token=b11e494e-d2a1-4ea5-92f7-4cc2b980b1bb" alt=""><figcaption></figcaption></figure>

## Enabling the Solidity Compiler Optimizer

&#x20;         An additional method to reduce smart contract size is by enabling the Solidity optimizer.

&#x20;         According to the official Solidity documentation:\
&#x20;         The optimizer works by simplifying complex expressions, which helps lower both the size of the compiled code and its execution cost.

&#x20;         To activate the optimizer in Hardhat, simply add the following configuration to your `hardhat.config.ts` file:

```javascript
require("@nomicfoundation/hardhat-toolbox");
require("hardhat-contract-sizer");
require("dotenv").config();

module.exports = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      evmVersion: "paris",
    },
  },
  gasReporter: {
    enabled: true,
  },
  networks: {
    ICBTestnet: {
      url: "https://rpc1-testnet.icbnetwork.info",
      chainId: 73114,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
    ICBMainnet: {
      url: "https://rpc2-mainnet.icbnetwork.info",
      chainId: 73115,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
  },
};

```

&#x20;         With 1,000 runs result will be:

<figure><img src="https://1670530031-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNGHmFVO7DlELOLs4PSVs%2Fuploads%2F1EC83Ts41tUEtwxnEzZR%2Fimage9.png?alt=media&#x26;token=4aafdd84-f558-4125-9204-331fb94685db" alt=""><figcaption></figcaption></figure>

&#x20;         We decrease the number of runs from 1,000 to 200 and you can see some improvements for the `Car` contract:

<figure><img src="https://1670530031-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNGHmFVO7DlELOLs4PSVs%2Fuploads%2FOIzgMTzGoU7LmzNltwoo%2Fimage8.png?alt=media&#x26;token=cb5b6ece-b6b1-4ae3-8ab2-1bfb2b3681cd" alt=""><figcaption></figcaption></figure>

&#x20;         The contract size may have increased, but this trade-off typically leads to better runtime efficiency. A higher `runs` value in the optimizer setting makes execution cheaper, although it results in a more expensive deployment. For more details, refer to the official [Solidity documentation](https://docs.soliditylang.org/en/latest/).

## Final Thoughts

&#x20;         In this tutorial, you explored how to analyze and reduce smart contract sizes using the Hardhat development environment alongside the Hardhat Contract Sizer plugin. By understanding the impact of contract size, you've gained practical tools and techniques to write more efficient and maintainable Solidity code.

&#x20;         As you progress in your smart contract development journey, remember that optimizing for size is an ongoing process. Balancing bytecode size, gas costs, and code readability requires thoughtful design decisions throughout the development lifecycle.
