hedera-services icon indicating copy to clipboard operation
hedera-services copied to clipboard

Gas estimation does not work for a contract with internal transactions

Open fernandezlautaro opened this issue 5 months ago • 2 comments

Description

For a smart contract that has internal transactions the gas estimation seems to throw a lower number than expected, making it hard to properly estimate how much the gas limit should be to perform a successful call.

I'll be attaching a solidity contract InternalTxContract.sol and a small node application using ethers to show case how to reproduce this error, in which will first estimate the gas of the call to be performed, with that info will try to perform the call and fail, to later add an extra padding of 10% of the Hedera's estimation making it work.

Ideally I shouldn't need to add any extra padding, as the estimation should be good enough to perform the call.

Steps to reproduce

  1. Deploy the following InternalTxContract.sol smart contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract InternalTxContract {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // Function to receive Ether into this contract
    receive() external payable {}

    // Function to perform internal transaction
    function triggerInternalTransaction(address payable _to, uint256 _amount) public {
        require(msg.sender == owner, "Only owner can send internal transactions");
        require(address(this).balance >= _amount, "Insufficient balance in contract");

        _to.transfer(_amount);  // Internal transaction sending Ether to another address
    }

    // Function to get contract's balance
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

Mine is deployed here 0x32c9becf6618c7e6055f23ad77cea278ba3aa241 (testnet): https://hashscan.io/testnet/contract/0.0.4913824

  1. Fund/transfer the contract with some HBARs (at least 1, as the following test will try to send 1HBAR to a particular wallet)

  2. Create a node app app.js and install latest ethers (v6 as the time of filing this issue). In that node app put the following code

const { ethers } = require("ethers");
const { JsonRpcProvider, Wallet, Contract, parseEther } = require('ethers')

async function main() {
    const privateKey = "<my private key>";
    const contractABI = [
        "function triggerInternalTransaction(address payable _to, uint256 _amount) public",
        "function getBalance() public view returns (uint256)"
    ];
    
    const providerH = new JsonRpcProvider("https://testnet.hashio.io/api");
    const contractAddressH = "0x32c9becf6618c7e6055f23ad77cea278ba3aa241";

    const wallet = new Wallet(privateKey, providerH);
    const contract = new Contract(contractAddressH, contractABI, wallet);

    // Recipient address and amount to send
    const recipientAddress = "<my wallet address>";  
    const amountToSend = parseEther("0.0000000001"); //sending 1HBAR

    try {
        // Log the contract balance after the transaction
        const contractBalance = await contract.getBalance();
        console.log(`Initial Contract Balance: ${ethers.formatEther(contractBalance)} ETH`);

        // Estimate gas for the function call
        const gasEstimate = await contract.triggerInternalTransaction.estimateGas(recipientAddress, amountToSend);
        console.log(`Gas Estimate: ${gasEstimate.toString()}`);

        // Send the internal transaction that will fail
        const failedTx = await contract.triggerInternalTransaction(
            recipientAddress,
            amountToSend,
            {
                gasLimit: gasEstimate  // Using the estimated gas limit
            }
        );

        try{
            const failedReceipt = await failedTx.wait();
        } catch (error) {
            console.error("Failed sending transaction hash:", error.receipt.hash);
        }
        
        const increasedGasEstimate = gasEstimate * 110n / 100n; // 10% increase
        console.log(`Padded Gas Estimate: ${increasedGasEstimate.toString()}`);
        // Send the internal transaction
        const successfulTx = await contract.triggerInternalTransaction(
            recipientAddress,
            amountToSend,
            {
                gasLimit: increasedGasEstimate  // Using the 10% padded one
            }
        );
        const successfuldReceipt = await successfulTx.wait();
        console.log("Transaction successful with hash:", successfuldReceipt.hash);

        // Log the contract balance after the transaction
        const contractBalanceAfter = await contract.getBalance();
        console.log(`Contract Balance After failed successful tx: ${ethers.formatEther(contractBalanceAfter)} ETH`);
    } catch (error) {
        console.error("Uncaught error:", error);
    }
}

main(); 
  1. Run the node app node app.js. You will get something similar to the following output:
➜  internal-tx-test node app.js
Initial Contract Balance: 0.0000000009 ETH
Gas Estimate: 34075
Failed sending transaction hash: 0xe70cfb12720bc73b0bdc57099caecd2ded35687aa60eb448a640d3fe6a79ac20
Padded Gas Estimate: 37482
Transaction successful with hash: 0x300699593b8e555bd707a1bf2e50231012bf87adc18820eb444f4d4265955067
Contract Balance After failed successful tx: 0.0000000008 ETH
  1. If you go to the first and failed tx 0xe70cfb12720bc73b0bdc57099caecd2ded35687aa60eb448a640d3fe6a79ac20 you will see the error was INSUFFICIENT_GAS, while the second one and successful one tx 0x300699593b8e555bd707a1bf2e50231012bf87adc18820eb444f4d4265955067 went through perfectly fine

  2. Finally, just to be sure this gas estimation issue was only affecting internal tx of smart contracts I created another one that only stores a string, deployed it to testnet and call it from a similar node app and it was working fine (I can attach both solidity contract and node app for this one, but I believe it won't add much to this, I just wanted to be sure the test I performed worked for other scenarios to isolate the issue)

Additional context

No response

Hedera network

testnet

Version

latest

Operating system

macOS

fernandezlautaro avatar Sep 26 '24 14:09 fernandezlautaro