After I exposed OpenZeppelin smart contracts for wasting our gas on NFTs, people have started asking me to review their code or for advice. During one such review, I said:
Traditional programmers like to verify things before they use them, but that costs more gas. With Solidity, we should just use them, and revert when there isn’t enough.
And now, a few hours later, I’m developing a smart contract for a friend that exemplifies this ad absurdum. For this contract, I need to verify all of the following requirements while saving gas:
- that the current wallet (user) owns tokens on another contract
- that these tokens have not been used
- that their quantity is greater than or equal to another
… and at some point…
4. I need to mark them used
Exhibit A
function requireIntersection(uint[] memory tokens, uint needed) private {
uint[] memory wallet = IERC721( NFT_CONTRACT ).walletOfOwner( msg.sender );
for(uint t; t < tokens.length; ++t ){
if( isUsed[ tokens[t] ] )
continue; for(uint w; w < wallet.length; ++w ){
if( isUsed[ wallet[w] ] )
continue; if( tokens[t] == wallet[w] ){
isUsed[ tokens[t] ] = true;
if( --needed == 0 )
return;
else
break;
}
}
} revert( "Not enough tokens" );
}
Et voila! Here we have an efficient, light-weight, non-reentrant intersection algorithm.
Explanation:
This function starts by calling contract-to-contract to get a list of the users tokens. This will cost about 4,000 to 5,000 gas and provide us ALL of the users tokens they own. It’s better to pay this once for the whole set than to check each token individually.
Next, this function verifies that tokens being verified and tokens in the wallet are also unused, which effectively shrinks both lists.
Then, if the token is un-used and verified, mark it used* and decrement the quantity needed. When we decrement to zero, we can return successfully. But if we never reach zero, we don’t have enough tokens, and use revert() to fail the transaction.
*did you notice that this also protects against reentrancy?
-Squeebo