Skip to content

viaIR compiler causes 10M gas explosion with ONE keyword difference (bool private vs bool private transient) #16381

@slwyts

Description

@slwyts

Description

When using the IR compilation pipeline with Solidity 0.8.33, adding a simple bool private storage variable for reentrancy protection causes deployment gas to double from ~10M to ~20M.

The only difference is ONE keyword: transient

// ❌ ~20M gas - deployment fails
bool private _isExecutingMechanism;

// ✅ ~10M gas - deployment succeeds  
bool private transient _isExecutingMechanism;

Expected behavior: Adding a single bool storage variable should not cause ~10M gas increase during deployment.

Actual behavior: Deployment gas doubles from ~9.9M to ~19.9M, exceeding typical block gas limits.

Additional paradox: The bytecode with the problematic bool private is actually SMALLER (42,163 bytes) than with transient (42,398 bytes), but gas is 2x higher!

Environment

  • Compiler version: 0.8.33
  • Compilation pipeline (legacy, IR, EOF): IR (viaIR: true)
  • Target EVM version (as per compiler settings): default
  • Framework/IDE (e.g. Foundry, Hardhat, Remix): Hardhat 3.1.0
  • EVM execution environment / backend / blockchain client: Hardhat Network (EDR)
  • Operating system: Windows 11

Steps to Reproduce

Minimal Solidity Code

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract HAFToken is ERC20 {
    // Many other storage variables (mappings, uints, addresses, etc.)
    // ... approximately 30+ storage slots ...
    
    // BUG: This single bool causes ~10M gas increase during deployment
    // Change to `bool private transient` to fix
    bool private _isExecutingMechanism;
    
    constructor(
        address _usdt,
        address _factory, 
        address _router,
        address _hashFiContract
    ) ERC20("HAF Token", "HAF") {
        // Complex constructor with multiple external calls
        // Creates Uniswap pair, sets up initial state, etc.
        _mint(msg.sender, 210_000_000 * 10**18);
    }
    
    function _update(address from, address to, uint256 amount) internal virtual override {
        _triggerLazyMechanisms();
        super._update(from, to, amount);
    }
    
    function _triggerLazyMechanisms() internal {
        // Simple reentrancy guard pattern
        if (_isExecutingMechanism) return;
        _isExecutingMechanism = true;
        
        _tryDailyBurn();
        _tryAutoBurn();
        
        _isExecutingMechanism = false;
    }
    
    function _tryDailyBurn() internal {
        // Complex burn logic that may trigger recursive _update calls
    }
    
    function _tryAutoBurn() internal {
        // Complex auto-burn logic with potential swap operations
    }
}

Compiler Settings (hardhat.config.ts)

solidity: {
  version: "0.8.33",
  settings: {
    optimizer: {
      enabled: true,
      runs: 1,
    },
    viaIR: true,  // Required - bug only occurs with IR pipeline
  },
}

Full Reproduction Repository

git clone https://github.com/slwyts/HashFi.git
cd HashFi
git checkout HAFToken
npm install

Test with transient (Working - ~10M gas):

npx hardhat clean
npx hardhat compile
npm run testnet

Reproduce the bug - Edit contracts/HAFToken.sol line 202:

// Change FROM:
bool private transient _isExecutingMechanism;

// Change TO:
bool private _isExecutingMechanism;

Recompile and test (Failing - ~20M gas):

npx hardhat clean
npx hardhat compile
npm run testnet

Results

Code Version Deployment Gas Bytecode Size (init) Result
bool private _isExecutingMechanism 19,945,987 42,163 bytes ❌ Exceeds gas cap
bool private transient _isExecutingMechanism 9,923,481 42,398 bytes ✅ Deploys

Actual Logs

✅ With bool private transient (~10M gas):

Contract deployment: HashFi
  From:        0xa4b76d7cae384c9a5fd5f573cef74bfdb980e966
  Value:       0 ETH
  Gas used:    9923481 of 12532761
  Block #5:    0x18ab...

❌ With bool private (~20M gas):

Error HHE10409 in plugin hardhat-ignition: Gas estimation failed: 
transaction gas limit (19945987) is greater than the cap (16777216)

Analysis

The issue appears related to how the IR pipeline handles:

  1. Storage variable SLOAD/SSTORE in functions called from _update override
  2. Cross-function inlining - _triggerLazyMechanisms() is called from _update(), which is called from many places
  3. Control flow analysis for storage slots with read-modify-write patterns

The fact that transient storage (TLOAD/TSTORE) doesn't have this issue suggests different optimization paths for persistent vs transient storage in this specific pattern.

Relevant Commits

  • One-line fix: slwyts/HashFi@7048055
  • Test commit: test: reproduce deployment gas overflow - bool private reentrancy lock causes gas from 10M to 20M
  • Fix commit: fix: use transient storage for reentrancy lock to avoid viaIR compiler gas explosion

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions