Solidity String Storage – Is It Cheaper to Store a String or Convert It to bytes32?

bytesbytes32gassolidity

For example, in the code below, would it be cheaper to store the string in a mapping? Or to convert it to bytes32 and store it as bytes32 then convert it back to a string whenever I call it with JavaScript?

mapping(string => bool) _tokenExists;

function test(string memory _str) public {
    _tokenExists[_str] = true;
}

Or

mapping(bytes32 => bool) _tokenExists;

function test(string memory _str) public {
    _tokenExists[abi.encodePacked(_str)] = true;
}

Which one of these would be cheaper & better to use and why?

PS: Is there a way I can find out on my own for the future which would be cheaper by doing my own gas estimates?

Best Answer

First of all, abi.encodePacked(_str) will convert the string _str to its UTF-8 bytes representation, and not to bytes32. bytes is a dynamically-sized byte array, as is string. Since Solidity 0.8.5 it’s possible to cast bytes into bytes32, but you need to pay attention because anything beyond 32 bytes will be chopped off.

So if your string’s UTF-8 bytes representation is 32 bytes or shorter, you can place it in a bytes32 via bytes32(abi.encodePacked(_str)), whereas if your string is longer than 32 bytes, you can compress it into a bytes32 via keccak256(abi.encodePacked(_str)).


From the Solidity docs:

The value corresponding to a mapping key k is located at keccak256(h(k) . p) where . is concatenation and h is a function that is applied to the key depending on its type:

  • for value types, h pads the value to 32 bytes in the same way as when storing the value in memory.
  • for strings and byte arrays, h computes the keccak256 hash of the unpadded data.

p above is the “slot position” for the mapping variable (your _tokenExists), so within a contract it is constant. For this example, suppose that p = 0. So we have:

Type of key k Value slot position
bytes32 keccak256( k . 0 )
bytes or string keccak256( keccak256(abi.encodePacked(k)) . 0 )

So for mappings with bytes or string key-type, accessing a mapping value invokes an extra keccak256. But invoking keccak256 is cheap enough to not let it influence your design decisions. Rather you should use string or bytes32 depending on what is semantically more suitable for your contract.


A simple way to test the theory above “on your own, for the future” is by loading in Remix IDE a test-contract, deploying it to the JavaScript VM Environment, and invoking the different functions with the same input strings, trying both short and long strings, and comparing the different functions’ execution cost, e.g.:

pragma solidity ^0.8.5;

contract Test {
    mapping(string => bool) _tokenExists1;
    function test1(string memory _str) external {
        _tokenExists1[_str] = true;
    }

    mapping(bytes => bool) _tokenExists2;
    function test2(string memory _str) external {
        _tokenExists2[abi.encodePacked(_str)] = true;
    }

    mapping(bytes32 => bool) _tokenExists3;
    function test3(string memory _str) external {
        _tokenExists3[keccak256(abi.encodePacked(_str))] = true;
    }

    mapping(bytes32 => bool) _tokenExists4;
    function test4(string memory _str) external {
        _tokenExists4[bytes32(abi.encodePacked(_str))] = true;
    }
}
Related Topic