2

Consider the following smart contract:

pragma solidity ^0.8.19;

contract Test {
    mapping (address => bool) registered;

    function test(address[] calldata addresses) external {
        uint256 l = addresses.length;
        for (uint256 i; i < l; ) {
            registered[addresses[i]] = true;

            unchecked {
                ++i;
            }
        }
    } 
}

I am seeing a behavior which I can not fully understand.

When I pass the parameter: [0xd60DfF1c0f7583377D38b19594151FDffa792A5E,0xd60DfF1c0f7583377D38b19594151FDffa792A5E]

the spent gas is 47626.

When I pass the parameter [0xd60DfF1c0f7583377D38b19594151FDffa792A5E,0x7000906cDa408120dC5231758A3fA7f47d55C59F]

the spent gas is 67374.

Even though both have 2 addresses, when array consists of same addresses, it is cheaper. What could be the reasoning behind this?

What gets even weirder is that, these are the first run gas spendings. After executing once, the gas spent on the following txs drops quite a lot, with second case still being slightly expensive than the first.

My instinct is that this is relevant to storing something for the first time in mapping, and caching if a key's hash (or however the position of the value is determined) is already calculated previously.

But I could not find a detailed information about this. I would appreciate if anyone could explain the reasoning behind this behavior.

PS: I tested this on Avalanche-Fuji. I used Remix IDE to both deploy and run the contract. I used Metamask as my wallet provider. The optimization was enabled with 10000000 runs. EVM version was default.

1 Answer 1

2

The first time you initialize a storage slot in the evm, you will be charged 20k gas for the storage. Because your second list of arguments contains one of the values in the first list, that storage is not initialized, and you therefore do not incur the 20k gas cost of initializing it.

If you try subsequent calls with completely different addresses in the list, you should expect to see around 67k gas spent again. Also you can assume that each extra address you pass into the array of this function will add about 20k+ gas.

Gas costs of storage are one of the most important aspects of gas optimization in solidity. The figures are

New storage creation: 20k gas Storage modification: 5k gas (Changing the data inside already existing storage)

This is also why we try and pack storage tightly in solidity. Storage comes in 32 byte increments in terms of gas cost, so doing things such as tightly packing structs can save gas.

A link about solidity struct packing, this might also help you understand storage slots in the evm: https://fravoll.github.io/solidity-patterns/tight_variable_packing.html

5
  • That makes a loot of sense. So what would be the case for nested mappings. Is it creating 2 slots in that case to store 1 value referenced by 2 new keys for the first time? And then if I pass a new second key but use the same first key I used previously, is it gonna just create 1 slot? Mar 11, 2023 at 22:31
  • 1
    Yes exactly. When you call the function the first time, you are storing two booleans, but because they are in a mapping they are separate in memory and both charge you 20k gas. When you call the function the second time, one of the booleans is already created and set to true, so it is neither modified or created. In that case you just create one new storage slot for the new value. You could also expand this out to any number of already existing and not yet filled values in your mapping. Each new key is going to charge you 20k gas, and each other will charge you nothing.
    – Bruce
    Mar 11, 2023 at 22:37
  • 1
    It would charge you 5k gas if you were to change the value what a key maps to. For example, if on the second call the function flipped the value of the bool in storage to false, you would be charged 5k gas for a storage modification, since the storage already existed. If you are modifying the value but "changing" true to true in your case, the evm is smart enough to figure out that is not actually storage changing and not charge you the gas.
    – Bruce
    Mar 11, 2023 at 22:38
  • You were really helpful thank you. One last question I have in this topic. If mappings inside mappings causes more than one slot creation for the first initialization of a value, then why there is no optimization of packing keys together? So if you have address->bytes32->bool mapping lets say, address and bytes32 keys can be packed and hashed, this way only 1 slot is created. Mar 11, 2023 at 23:11
  • I'm not totally sure how doubly nested mapping create storage slots, they might compile to do that by default. If not though, you can always manually hash the values together with keccak256 and use that value to index into your mapping. But solidity may do this under the hood, you would need to test that or research into it
    – Bruce
    Mar 11, 2023 at 23:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.