Why doesn't `EnumerableSet.sol` includes bytes set?

In one of my use case, I have a mapping that maps a string to an array of bytes. The reason for using bytes instead of bytes32 is because this bytes value is obtained by using abi.encode for multiple values. I am not using abi.encodePacked because there's no native way of decoding a packed data in solidity.

Now EnumerableSet doesn't have a set for bytes.

To make it work with my use case, I copied the code related to bytes32 and changed it bytes.

The code:

pragma solidity ^0.8.20;

library EnumerableSet {
    struct Set {
        // Storage of set values
        bytes [] _values;
        // Position is the index of the value in the `values` array plus 1.
        // Position 0 is used to mean a value is not in the set.
        mapping(bytes value => uint256) _positions;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function _add(Set storage set, bytes memory value) private returns (bool) {
        if (!_contains(set, value)) {
            set._values.push(value);
            // The value is stored at length-1, but we add 1 to all indexes
            // and use 0 as a sentinel value
            set._positions[value] = set._values.length;
            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function _remove(Set storage set, bytes memory value) private returns (bool) {
        // We cache the value's position to prevent multiple reads from the same storage slot
        uint256 position = set._positions[value];

        if (position != 0) {
            // Equivalent to contains(set, value)
            // To delete an element from the _values array in O(1), we swap the element to delete with the last one in
            // the array, and then remove the last element (sometimes called as 'swap and pop').
            // This modifies the order of the array, as noted in {at}.

            uint256 valueIndex = position - 1;
            uint256 lastIndex = set._values.length - 1;

            if (valueIndex != lastIndex) {
                bytes  memory lastValue = set._values[lastIndex];

                // Move the lastValue to the index where the value to delete is
                set._values[valueIndex] = lastValue;
                // Update the tracked position of the lastValue (that was just moved)
                set._positions[lastValue] = position;
            }

            // Delete the slot where the moved value was stored
            set._values.pop();

            // Delete the tracked position for the deleted slot
            delete set._positions[value];

            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function _contains(Set storage set, bytes memory value) private view returns (bool) {
        return set._positions[value] != 0;
    }

    /**
     * @dev Returns the number of values on the set. O(1).
     */
    function _length(Set storage set) private view returns (uint256) {
        return set._values.length;
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function _at(Set storage set, uint256 index) private view returns (bytes memory ) {
        return set._values[index];
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function _values(Set storage set) private view returns (bytes [] memory) {
        return set._values;
    }

    // bytes Set

    struct bytesSet {
        Set _inner;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function add(bytesSet storage set, bytes memory value) internal returns (bool) {
        return _add(set._inner, value);
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function remove(bytesSet storage set, bytes memory value) internal returns (bool) {
        return _remove(set._inner, value);
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function contains(bytesSet storage set, bytes memory value) internal view returns (bool) {
        return _contains(set._inner, value);
    }

    /**
     * @dev Returns the number of values in the set. O(1).
     */
    function length(bytesSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function at(bytesSet storage set, uint256 index) internal view returns (bytes memory  ) {
        return _at(set._inner, index);
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function values(bytesSet storage set) internal view returns (bytes [] memory) {
        bytes [] memory store = _values(set._inner);
        bytes [] memory result;

        /// @solidity memory-safe-assembly
        assembly {
            result := store
        }

        return result;
    }

}

This is exactly same as OZ EnumerableSet just the bytes32 has been changed to bytes.

It also seems to working fine at a high level glance.

But I want to know if this is security wise safe to use, if yes then why this bytes set is not included in OZ EnumerableSet librar?

Appreciate any insights on this approach.

1 Like

Hello @Zartaj

bytes (and string are not value type). This makes manipulating them more expensive.

Under the hood, mapping(bytes value => uint256) is stored as mapping(bytes32 hashOfValue => uint256).

We may consider adding a bytes version if we see a clear need for that, but in the meantime, you should probably just use a combinasion of a bytes32 enumerable set with a mapping(bytes32 => bytes) store for the lookups.

Thank you for replying.

Apart from gas usage, can there be any other problems with the library I pasted above.

For reference I am using it this way:

mapping :
mapping(string => EnumerableSet.bytesSet) internal PGPToWallet;

usage:

function registerUserPGP(bytes calldata _data, string calldata _pgp) external {
  
        (,,, address _nft, uint256 _id,) = abi.decode(_data, (string, string, uint256, address, uint256, uint256));
         require(IERC721(_nft).ownerOf(_id) == msg.sender, "NFT not owned");

         PGPToWallet[_pgp].add(_data);
}

There are obviously other operations inside this function, but this is how I am using the bytesSet.
Can you elaborate a bit how can I utilize a combination, if that's better than this approach.

Do you have any piece of code that reads the EnumerableSet at all ? If no, you should probably not store it and just emit an event.

I don't thing there any security risk with what you do ... but I also think you should probablty not be storing that at all.

Rule of thumbs is: do not write something to the blockchain state if you dont plan for a smart contract to read it from the blockchain. Offchain tools (wallet, websites, ...) can read from the events.

Yes, I have view functions to read it, using the library itself for this.

function containsValue(string memory key,bytes memory value) external view returns (bool) {
      return PGPToWallet[key].contains(value);
  }

  // Function to get the count of values in the set associated with a string key
  function getCount(string memory key) external view returns (uint256) {
      return PGPToWallet[key].length();
  }

  // Function to get the value at a specific index in the set associated with a string key
  function getValueAtIndex(string memory key, uint256 index) external view returns (bytes memory) {
      require(index < PGPToWallet[key].length(), "Index out of bounds");
      return PGPToWallet[key].at(index);
  }

  // Function to enumerate over the values in the set associated with a string key
  function enumerateValues(string memory key) external view returns (bytes[] memory) {
      uint256 count = PGPToWallet[key].length();
      bytes[] memory values = new bytes[](count);
      for (uint256 i = 0; i < count; i++) {
          values[i] = PGPToWallet[key].at(i);
      }
      return values;
  }

Cool, I'll think of another approach, and update here as well.