Advanced Solidity to Save Money on Gas Fees

Solidity Compiler Configuration, and Freeing Storage for EVM Gas Refund

recash-lab---Compiler-Config-&-Free-Storage----featured-image

This last post in our series of gas-saving articles will discuss simple Solidity functionalities that developers often ignore. We will explore two significant Solidity mindsets for gas optimization: Freeing Storageand Solidity’s “Optimizer.

Freeing Storage Pattern

Certain storage variables within a smart contract may become obsolete as time passes, yet they persistently consume valuable space on the blockchain. However, the Ethereum network presents a solution to this issue by encouraging the reduction of blockchain bloat by providing gas refunds to free up storage space. Using Solidity’s delete keyword, developers can use this incentive to eliminate unnecessary storage variables. This proactive approach enables developers to streamline their contracts, free blockchain congestion, and optimize resource utilization on the Ethereum network.

Example and Scenario

Let’s look at the example of a Uniswap v3 factory to familiarize ourselves with how the creators use the delete keyword to free storage and save gas when deploying every new trading pool.

Solidity
pragma solidity ^0.8.0;

contract UniswapV3Factory {
    /// @inheritdoc IUniswapV3Factory
    function createPool(
        address tokenA,
        address tokenB,
        uint24 fee
    ) external override noDelegateCall returns (address pool) {
        require(tokenA != tokenB);
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 
        require(token0 != address(0));
        int24 tickSpacing = feeAmountTickSpacing[fee];
        require(tickSpacing != 0);
        require(getPool[token0][token1][fee] == address(0));
        pool = deploy(address(this), token0, token1, fee, tickSpacing);
        getPool[token0][token1][fee] = pool;
        // populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses
        getPool[token1][token0][fee] = pool;
        emit PoolCreated(token0, token1, fee, tickSpacing, pool);
    }
    
    function deploy(
        address factory,
        address token0,
        address token1,
        uint24 fee,
        int24 tickSpacing
    ) internal returns (address pool) {
        parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});
        pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
        delete parameters;
    }
}

The deploy function uses a temporary Parameters structure to store the variables needed for contract creation. After the function deploys the new contract, it deletes the parameter’s struct using the delete keyword. This process ensures that we free up the storage immediately after the function uses the parameters and they’re no longer needed. By deleting the parameters, the function ensures that it does not leave any unnecessary data in storage, which can help reduce the overall gas cost.

Knowing that you can receive a gas refund of 15,000 units for resetting a storage slot to zero is beneficial. However, you must utilize this refund within the same transaction, and carrying it over to future transactions will not be possible. Specifically, the gas refund can cover up to half of the total gas consumed, allowing you to offset some of your gas costs through efficient storage cleanup, enabling more operations within the same transaction.

It’s important to note that the refunded gas will not be returned to you in Ether but instead helps reduce the immediate gas expenditure.

Solidity Optimizer

Optimizing Solidity code to save gas is a considerable feat. It’s a delicate balancing act for developers, who must ensure their smart contracts are efficient and uphold their intended purpose. One standard tool in the developer’s arsenal is the Solidity Optimizer. Integrated into Solidity compilers, this feature works behind the scenes to fine-tune code efficiency through various optimization techniques. However, relying solely on the optimizer isn’t the silver bullet. While it does an admirable job, developers should augment its efforts with manual design patterns. These patterns target inefficiencies that may slip past the compiler’s gaze, ensuring a thorough optimization approach for smart contracts.

The Solidity documentation offers a comprehensive section dedicated to the Optimizer and its various use cases. As the Optimizer is undergoing continuous development and updates, it’s crucial to stay updated with the latest information from the docs. By doing so, you can actively participate in the evolution of this useful tool, which we will introduce in this section.

Solidity optimization is a multi-layered process that enhances code efficiency and reduces gas costs. Optimizations take place at three distinct levels of execution, starting with code generation, followed by transformations on Yul IR (Intermediate Representation) code, and culminating in opcode-level optimizations. The opcode-based optimizer simplifies opcodes, combines similar code sets, and removes unused code. It can be activated through the `—optimize` option and the `solc` as a command line argument.

Solc —optimize

If you use tools like Truffle to develop or deploy your contracts, update your truffle-config.js to the example configuration below. It’s worth noting that you can experiment with different values for the optimizer.runs parameter to find the optimal balance between gas savings and compilation time.

JavaScript
module.exports = {
  // ...
  compilers: {
    solc: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  }
}

The Solidity compiler (solc) doesn’t just generate bytecode; it does so in an advanced way by utilizing an intermediate representation (IR). Meanwhile, the Yul-based optimizer operates across function calls, allowing for more advanced optimizations like reordering and removing side-effect-free function calls. Targeting the solidity source, we can use solc --ir-optimized --optimize for an optimized Yul IR or alternatively for a stand-alone Yul mode use solc --strict-assembly --optimize. On the other hand, for tools like truffle or hardhat we can look deeper into the Solidity compiler’s input and output JSON description and this cutting-edge compilation mode, known as ‘via IR,’ can be activated through the viaIR setting in your Truffle or Hardhat configuration.

So, here is what we can set in a truffle configuration.

JavaScript
module.exports = {
  // ...
  compilers: {
    solc: {
      version: "0.8.0",  // Specify the version of solc you want to use
      settings: {
        optimizer: {
          enabled: true,
          runs: 200
        },
        viaIR: true,  // This enables the Yul IR optimizer, which is False by default.
        outputSelection: {
          "*": {
            "*": ["ir", "irOptimized"]  // Output both IR and optimized IR
          } 
        }
      }
    }
  }
};

The ir value in the outputSelection is the Yul intermediate representation of the code before optimization and irOptimized which is the Yul-optimized version. Also, Truffle doesn’t have a direct way to enable --strict-assembly mode through the configuration yet. You might need to use a custom script or a plugin to achieve this option.

If you use Hardhat, the following example is configured for the compiler in your hardhat.config.js or hardhat.config.ts file. Currently, the viaIR option performs optimally when used with the Solidity optimizer. However, Hardhat’s functionality notably features like stack traces, is more reliable when the Optimizer is turned off. Consequently, full support for the viaIR option in Hardhat is still a work in progress. Despite these limitations, you can enable viaIR to compile your contracts and run tests, but be aware that certain Hardhat features may not function perfectly.

JavaScript
module.exports = {
  // ...
  solidity: {
    version: "0.8.0",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
        details: {
          yulDetails: {
            optimizerSteps: "u", // Use all optimization steps
          },
        },
      },
      viaIR: true,
    }
  }
};

Final words

As Solidity evolves, staying informed about the latest optimization techniques and compiler updates is crucial. By combining these strategies, you can significantly enhance the performance and cost-effectiveness of your smart contracts, contributing to a more efficient and scalable ecosystem. Keep experimenting, stay updated, and always optimize your Solidity code for the best results.

Exit mobile version