The reason why ethereum internal transactions might fail
Tags: solidity, ethereum, blockchain, gas
Published: 2022-04-28 20:12:15

**UPDATE:** Mystery solved! I've updated this blog post with the explanation.

Hi there!
I've been doing some [Ethernaut](https://ethernaut.openzeppelin.com/) challenges and ended up having some trouble on #9: [King](https://github.com/OpenZeppelin/ethernaut/blob/master/contracts/contracts/levels/King.sol).

The solution for the challenge itself is pretty straight forward, we just have to create a smart contract without receive and fallback functions so the King contract will fail to transfer ether back when trying to reclaim kingship.

However, this new smart contract won't be able to first claim kingship with the `.call` return values are not handled. That's because the internal `.transfer` transaction will run out of gas - and I have no idea why.

![out of gas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t3i9ygg2af0o4gzlu711.png)

Here, let me simplify the contracts and provide some code and links.

## 📝 Contracts

In the following code snippet, we have 4 contracts: `Sender`, `SenderWithRequire`, `SenderWithEmit` and `Receiver`.
Here, `Receiver` will play the [King](https://github.com/OpenZeppelin/ethernaut/blob/master/contracts/contracts/levels/King.sol) contract role.

```solidity
// SPDX-License-Identifier: None
pragma solidity 0.6.0;

contract SenderWithRequire {
    function send(address payable receiver) public payable {
        (bool success,) = receiver.call.gas(10000000).value(msg.value)("");
        require(success, "Failed to send value!");
    }
}

contract SenderWithEmit {
    event Debug(bool success);
    function send(address payable receiver) public payable {
        (bool success,) = receiver.call.gas(10000000).value(msg.value)("");
        emit Debug(success);
    }
}

contract Sender {
    function send(address payable receiver) public payable {
        receiver.call.gas(10000000).value(msg.value)("");
    }
}

contract Receiver {
    bool public hasReceived;
    receive() external payable {
        hasReceived = true;
        payable(address(0)).transfer(msg.value);
    }
}
```

## 🔁 Transactions

If we deploy and trigger `send` functions for both `SenderWithEmit` and `SenderWithRequire`, the `Receiver` will receive the value correctly and transfer it to the address 0 as expected.

### SenderWithEmit

Etherscan:
https://rinkeby.etherscan.io/tx/0x99fea99167ed4353ab55486bb9ac55a1779e923bbe91ffbb1675b46a68b657ef
EthTx.Info:
https://ethtx.info/rinkeby/0x99fea99167ed4353ab55486bb9ac55a1779e923bbe91ffbb1675b46a68b657ef

![SenderWithEmit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wwfdgyihmbyycgqbx935.png)

### SenderWithRequire

Etherscan: 
https://rinkeby.etherscan.io/tx/0x28f185043e771486e1bdc10810004214793606e9ff2345279f1d7a9898992e62
EthTx.Info: 
https://ethtx.info/rinkeby/0x28f185043e771486e1bdc10810004214793606e9ff2345279f1d7a9898992e62

![SenderWithRequire](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mdwvnsvdnpuhanuhb9j2.png)
 

But if we try to send ether using the `Sender` contract, it will run out of gas in its internal transfer transaction

### Sender (out of gas)

Etherscan: 
https://rinkeby.etherscan.io/tx/0xc25f37922c439d4fc22cfd06f0c493a89af4517e1ec258658bc2990327a66976
EthTx.Info: 
https://ethtx.info/rinkeby/0xc25f37922c439d4fc22cfd06f0c493a89af4517e1ec258658bc2990327a66976

![Sender](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lpmv9oxguxc7nywykanc.png)
 
Which is pretty odd, since it should spend less gas than the previous implementations.

## 💡 The explanation

Thanks to [Ernesto Garcia](https://twitter.com/ernestognw) and his contacts on [OpenZeppelin](https://openzeppelin.com), I was able to understand what was happening. 

Metamask was not obtaining the Gas Limit value provided by Remix and therefore called [eth_estimateGas](https://www.quicknode.com/docs/ethereum/eth_estimateGas) to figure out how much its value should be used for the transaction.

It turns out that this estimate only takes into account the main transaction - the `send` function itself - and not the underlying transaction `transfer`.

In the Metamask screenshot below we can see that the provider's estimate was **⛽ 37,433 gas**.

![Metamask Gas Limit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uuhvgyf0n6ucrqzeftvt.png)

And changing it to **⛽ 374,330 gas**, the transaction was able to complete with success.

Etherscan:
https://rinkeby.etherscan.io/tx/0x85b7e7cee51055d91b5f1d20f3d710388f65406b46077c868eedbef2435e1ce2 

![etherscan print](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hirofluszq9ggh41uitu.png)

Take a look at the `Gas Limit & Usage by Txn` fields below and notice how the [failing transaction](https://rinkeby.etherscan.io/tx/0xc25f37922c439d4fc22cfd06f0c493a89af4517e1ec258658bc2990327a66976) was consuming all the gas limit and the [succeeding txn](https://rinkeby.etherscan.io/tx/0x85b7e7cee51055d91b5f1d20f3d710388f65406b46077c868eedbef2435e1ce2 ) used only 16% of the **⛽ 374,330 gas limit**.

![Gas Limit and Usage comparison](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39nw331eikmvqz1qzucf.png)

What about the other Sender contracts with `require` and `emit`?

Checking the [SenderWithRequire transaction](https://rinkeby.etherscan.io/tx/0x28f185043e771486e1bdc10810004214793606e9ff2345279f1d7a9898992e62) again, it's possible to see that the gas estimate was set to **⛽ 45,150 gas**, which was 94% of the gas the consumed in the transaction. 

That's why they were always working; thanks to the extra code, their gas limit estimates were just enough to handle the internal `transfer` call.

![SenderWithRequire transaction](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hi4jum5g3ce9jl25zoq2.png)
 

## ⚙️ The internal works

For further investigation, we can take a look at how the provider estimates gas limit.

{% stackoverflow https://stackoverflow.com/a/55431746 %}

According to the answer to the StackOverflow question above,  [geth](https://geth.ethereum.org/) uses a binary search to try finding a minimal gas to run the transaction on the given block number.

The implementation for this behavior can be validated in the following code from [transaction_args.go](https://github.com/ethereum/go-ethereum/blob/f94e23ca66eef8fdac2473ce99ca6ad57324aaa2/internal/ethapi/transaction_args.go#L146)

```go
// Estimate the gas usage if necessary.
if args.Gas == nil {
	// These fields are immutable during the estimation, safe to
	// pass the pointer directly.
	data := args.data()
	callArgs := TransactionArgs{
		From:                 args.From,
		To:                   args.To,
		GasPrice:             args.GasPrice,
		MaxFeePerGas:         args.MaxFeePerGas,
		MaxPriorityFeePerGas: args.MaxPriorityFeePerGas,
		Value:                args.Value,
		Data:                 (*hexutil.Bytes)(&data),
		AccessList:           args.AccessList,
	}
	pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
	estimated, err := DoEstimateGas(ctx, b, callArgs, pendingBlockNr, b.RPCGasCap())
	if err != nil {
		return err
	}
	args.Gas = &estimated
	log.Trace("Estimate gas usage automatically", "gas", args.Gas)
}
```

The `DoEstimateGas` function is declared at [api.go](https://github.com/ethereum/go-ethereum/blob/f94e23ca66eef8fdac2473ce99ca6ad57324aaa2/internal/ethapi/api.go#L995) and contains the binary search logic mentioned before.

According to Ernesto, we can simplify the flow as the following:

1. Metamask (or any provider) receive a transaction with no gasLimit
2. If the provider is able to fill the gasLimit (like geth), it'll do it. Otherwise, it'll forward an eth_estimateGas call to a node and use the value returned
3. Send the transaction with the gasLimit gotten

## 🏁 Conclusion

A somewhat simple Ethernaut challenge ended up uncovering a fascinating gas related behavior that helped me to understand how Ethereum transactions work.

I also had the opportunity to meet and discuss the topic with Ernesto, a friendly OpenZeppelin developer. Thanks buddy!

## ⛓️ References

* [EVM Call opcode](https://github.com/wolflo/evm-opcodes/blob/main/gas.md#aa-call-operations)
* [Members of Address Types](https://docs.soliditylang.org/en/v0.6.0/units-and-global-variables.html#members-of-address-types)
* [Go Ethereum](https://geth.ethereum.org/)