The ERC721A contract’s origins
When it launched, Azuki made a great deal of gas fees optimization.
At this time, ether was high in value, around $4k, and the gas war was raging.
Most collections were using the OpenZeppelin implementation that could cost hundreds of dollars per minted item.
They came with a new, and quite revolutionary at this time, way of storing NFT information making the mint cost very low and almost independent from the number of minted items.
Minting 10 items was almost the same price as 1.
Out of the original contract, the team created an open source project that kept improving gas savings. You can find a complete guide on how to use it here.
One of the improvements that were not in the original contract, is the use of custom errors.
In the original contract, you’ll see this kind of code, a pretty standard require instruction :
require(_exists(tokenId),"ERC721Metadata: URI query for nonexistent token");
But if you look at the ERC721A.sol contract, you’ll notice that the code has a little bit changed :
if (!_exists(tokenId)) revert URIQueryForNonexistentToken();
What is that?
Well, this is a custom error.
Why use custom errors?
In solidity, you can create custom errors that are exported in the ABI file.
Why did they do that?
Short answer: it saves gas.
Long answer:
Let’s compare two implementations.
ShortError will use a custom error and StandardError a classic require assertion.
contract ShortError {
error InvalidCode();
bool public found = false;
function solve(uint256 code) external {
if(code != 42) revert InvalidCode();
found = true;
}
}
contract StandardError {
bool public found = false;
function solve(uint256 code) external {
require(code == 42, "Wrong code");
found = true;
}
}
Now let’s compare the gas cost :
Contract | Deployment | Failure | Success |
---|---|---|---|
ShortError | 117,015 | 21,666 | 43,800 |
StandardError | 130,263 | 21,920 | 43,800 |
Saving | 13,248 | 254 | 0 |
You’ll notice that in case of success, the gas cost will be exactly the same.
But in case of error, you’ll save some gas.
On top of this, deployment is much cheaper.
In that case, you save more than 10k of gas per error message.
Right, this won’t change the world, but hey, that’s a good saving for a decent compromise.
What compromise?
Well, in case of error, the message will be less user-friendly.
Custom errors and user experience
In a standard require error, you would get this error message :
Usually, the error message is directly returned to the user.
You would display “execution reverted: Wrong code” in that case.
But with a custom error, you’ll get this kind of message :
You have no details about the reason why the transaction will fail.
Not very user-friendly, right?
If you want to go further, you’ll have to use the selector provided in the data field to find which error happened and translate it into a friendly error message.
If you look at the standard ‘require’ transaction on etherscan, you’ll have the error displayed :
Whereas with the custom error , you won’t have many details in the transaction.
Unless you click on the top-right tiny button and look at the Geth debug trace 2:
Here you’ll find the selector of the error.
On top of this, you’ll find the values if you use parameters in the error.
Should you use custom errors?
To summarize, here are the pros and cons of using custom errors:
Pro :
+ saves gas on deployment
+ saves gas in case of error
+ more accurate debug if you use error parameters
Cons :
– More complex to display a friendly error message to the user
– A bit harder to debug and understand where the error comes from
Do you want to optimize your smart contracts’ gas fees? Hire me here