foundry-rs/foundry

Forge gas measurement adds a significant overhead when transferring native tokens (ETH) to an account

0xpolarzero opened this issue · 2 comments

Component

Forge

Have you ensured that all of these are up to date?

  • Foundry
  • Foundryup

What version of Foundry are you on?

forge 0.2.0 (2cf84d9 2024-02-07T00:15:49.638525000Z)

What command(s) is the bug in?

forge test --gas-report

Operating System

macOS (Apple Silicon)

Describe the bug

Overview

When transferring native tokens to a recipient, there is a significant gas overhead, compared to real-world values. Here, we're using GasliteDrop airdropETH function to measure gas usage.

As a reference, we're measuring the same interactions with Tevm, which shows results extremely close to real-world values (see directly on Etherscan).

Test

The test is as follows:

    function test_airdropETH() public {
        payable(user).transfer(value);
        vm.startPrank(user);

        address[] memory recipients = new address[](quantity);
        uint256[] memory amounts = new uint256[](quantity);
        for (uint256 i = 0; i < quantity; i++) {
            recipients[i] = vm.addr(2); // this is what we will modify for comparison
            amounts[i] = 0.001 ether;
        }

        gasliteDrop.airdropETH{value: value}(recipients, amounts);
        vm.stopPrank();
    }

Comparison

There are 3 different cases:

  • airdropping each time to a new address (Foundry);
  • airdropping each time to the same address (Foundry);
  • airdropping each time to a new address (Tevm, our reference).

Here are the results for each (in the format n (recipients): gas (+ increase).

Foundry test with the same address each time

Run the original test.

1: 35,132
2: 42,015 (+6,883)
3: 48,898 (+6,883)

1000: 6,911,249

Foundry test with a new address for each recipient

Update line 61

- recipients[i] = vm.addr(2);
+ recipients[i] = vm.addr(i + 1);

1: 35,132
2: 69,515 (+34,383)
3: 103,898 (+34,383)

1000: 34,383,749

Tevm (simulate the transaction and return the gas used)

1: 10,132
2: 19,515 (+9,383)
3: 28,898 (+9,383)

1000: 9,383,749

These are similar to values found in Etherscan transactions.

So the difference here is:

  • Foundry (same recipient) -> Foundry (different recipients): +27,500 gas per recipient;
  • Tevm -> Foundry (different recipients): +25,000 gas per recipient;
  • Tevm -> Foundry (same recipient): +2,500 gas per recipient.

I can only make a few guesses:

  • Foundry considers the cost of "initializing" a new account on the EVM, which adds a 22,500 gas overhead? (25,000 - 2,500);
  • Tevm ignores this cost, which is why it is similar to values on actual mainnet transfers, usually to accounts that are already initialized.

But there is still that additional 2,500 gas for each recipient, even when the account has already received some native token previously.


Some measurements with --debug, just for additional context, at the transfer call, show that:

  • the first transfer costs 34,300 gas;
  • the second transfer costs the same if it's a new address, or 6,800 gas if it's the same (-27,500).

CALLs are subject to cold account access (2500 gas) and initialization surcharges (25000 gas); see evm.codes for more info. It's unfortunately strictly always more expensive to send an empty account native tokens via smart contract than via plain transaction (in native gas units, at least).

I'm not sure why TEVM doesn't consider the cold+empty surcharges – but it should, if it's in the context of calling a smart contract like Gaslite Drop.

Thank you for your answer @emo-eth, that makes total sense—didn't know about the cold account access.