On-chain Transactions: Should Enumerable Data Structures Be For On-chain Use Only?

Written by fearsomelamb789 | Published 2022/03/10
Tech Story Tags: nft | blockchain | on-chain-transaction | blockchain-use-cases | blockchain-technology | nft-economy | onchain-nft | web-monetization

TLDREnumerable data structures are meant for on-chain discovery and not front-end consumption. This means other protocols can easily and cheaply iterate over an account’s NFTs. However, at minting time, it becomes more and more expensive to loop over such items. If a user has, say 10 items, it will become quite expensive to check the list of ids and go through each one of the ids, consequently increasing the number of external calls from the protocol contract.via the TL;DR App

As time moves on and more Solidity devs get their hands dirty with ERC721 and ERC1155 standards, they start paying more attention to how important immutability and on-chain data truly are.

However, one reiterating mistake that keeps being overlooked is the use of data structures intended for other protocols interactions and on-chain discovery.

It is becoming increasingly more common for smart contracts to loop over items in a for-loop during transactions in or at transfer time. Enumerable data structures, however, are meant for on-chain discovery and not frontend consumption. The use cases of data structures such as `EnumerableSet` derive from its utility when it comes to being discovered on-chain by other protocols in a “cheap” way. This means that other protocols can easily and cheaply iterate over an account’s NFTs.

However, at minting time, it becomes more and more expensive to loop over such items. Therefore, the good practice of Enumerable data structures is to use them only for on-chain use cases.

If we think about an account holding a few NFTs and an iteration involving a couple of transactions (remember the `for` loop iteration we mentioned), the chances of that function running out of gas will increase, even for an off-chain read.

If we go back to the basics, even if a function is declared as `virtual`, it will only be a “free read” function whenever items are not being transferred. If a user has, say 10 items, it will become quite expensive to check the list of ids and go through each one of the ids, consequently increasing the number of external calls from the protocol contract.

How to solve this problem

  1. Create a function that lists the account token ids
  2. Call this function off-chain
  3. Pass the token ids to the function that needs them and verify ownership in this function


function useTokenIds(
   address registry, 
   uint256[] calldata tokenIds
) external {
   address sender = msg.sender;
   uint256 tokenId;
   for (uint256 i; i < tokenIds.length; i++){
     tokenId = tokenIds[i];
     require(
     IERC721(registry).ownerOf(tokenIds[i]) == sender,
     “not owner of those tokenIds”
     );
     _doSomethingWithThisTokenId(registry, tokenId, sender)
 }
}

If the frontend ever needs that list of ids we can implement a getter function to go through all tokens and return those token ids we are interested in. Doing this we would avoid using data structures like Enumerable because we do not really need other on-chain protocols to know about it.

One could use TheGraph as well to index all items of the user, or even the APIs of some marketplaces like OpenSea.

EIP721 tells it right in the EIP:

Alternatives considered: remove the asset enumeration function if it requires a for-loop, return a Solidity array type from enumeration functions.


Written by fearsomelamb789 | Polymath keyboard ape
Published by HackerNoon on 2022/03/10