Better Programming

Advice for programmers.

Follow publication

Sending Static Calls to a Smart Contract With Ethers.js

Save millions in gas fees by checking whether a transaction will fail before sending it

Jelilat Anofiu
Better Programming
Published in
5 min readMar 17, 2022
Metamask Transaction confirmation screenshot

A few months ago, I got the above error message from Metamask while trying to confirm an Ethereum transaction. Metamask had just added the feature at the time, and I tweeted about how it is a great UX improvement.

Apparently, I had entered invalid input variables on my dApp while testing an interaction with the Uniswap Smart Contract. Out of curiosity, I sent the transaction anyways to see if it’ll actually fail — and yes, it did.

But, how did Metamask know that the transaction was going to fail? This was a question that lingered in my mind for a while till I found an answer to it.

If you are also a curious mind like me, follow along with this article and I’ll answer the “how” by showing you how to use ethers.js to check the validity of a transaction before sending it.

Blockchain Transactions

Blockchain transactions are immutable by default, which means that there’s no way to reverse a transaction that has already been confirmed. For instance, if you send ETH to the wrong Ethereum address, there is no way you can recover it. Unless the address owner is nice enough to send you a refund.

A way to prevent this is by double-checking an address before confirming the transaction or using ENS names that can easily be read. But what about other kinds of transactions (e.g. Minting NFTs)?

According to the Tubbycat NFT sale analysis by Buycoins Research, about 776 ETH (~2 million USD at the time) was spent on failed transaction fees. This is only a tiny fraction when it comes to how much is being lost by the entire ecosystem on failed transactions. Some of these transaction fees are burned, while others go to miners.

Although there are other factors like frontrunning attacks that could cause a pending transaction to eventually fail, some of the transaction failures can be prevented by checking its validity beforehand.

Static Calls

Rather than executing the state-change of a transaction, it is possible to ask a node to pretend that a call is not state-changing and return the result.

This does not actually change any state, but is free. This, in some cases, can be used to determine if a transaction will fail or succeed.

Let’s consider this example:

Alice wants to order a burger from Bob’s fast food restaurant but will have to drive down there to get it. Bob makes the best burgers in the County and he sells out really fast.

If Alice drives down to the restaurant after the burgers have sold out, she would have wasted her time, energy, and gas. Therefore, she needs to know if the burgers are still available before she heads to the restaurant.

Bob’s restaurant has a website where he updates the sales so that people can see how many burgers are left. If Alice checks the website and sees that the burgers have been sold out, she stays in and doesn’t waste her resources driving down.

Otherwise, she drives down to the restaurant and gets her favorite burger. This scenario is similar to how static calls are made to check whether a transaction may fail or not.

Making a Static Call with NodeJS

In this section, we’ll make a static call to the Uniswap V3 contract and attempt to transfer a liquidity position we don’t own.

Before getting into the code, make sure you have ethers installed. If you don’t, install it with the following command:

npm install --save ethers

After successful installation, open the Uniswap contract code on Etherscan. Scroll down to the contract ABI and copy it to the clipboard.

Uniswap V3 ABI
  • Create an abi.json file and paste the ABI into it.
  • Next, create a new file that’ll be used to make the statical call.
  • Import the ethers library and abi into your code.
const { ethers } = require('ethers');require('dotenv').config()const abi = require('./abi.json')
  • Define the contract address, signer, and initialize the contract.
const contractAddress = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88"const signer = new ethers.Wallet(   process.env.PRIVATE_KEY,
ethers.getDefaultProvider('mainnet')
);const contract = new ethers.Contract(contractAddress, abi, signer);

This example uses the default provider from ethers. However, when creating your app, make sure to sign up for your own provider on Infura to increase your request rate/limit and have access to call metrics.

  • Define the transaction variables.
const from = "0x66fe4806cD41BcD308c9d2f6815AEf6b2e38f9a3"const to = "0xC41672E349C3F6dAdf8e4031b6D2d3d09De276f9"const tokenId = 100

The from address is neither the owner nor approved spender of token 100 and therefore, can’t transfer it to another address. But let’s try to transfer it with the callStatic method to see the outcome.

const transaction = async () => {const a = await contract.transferFrom.staticCall(from, to, tokenId)   console.log(a)}transaction()

When you try this, you should get an error similar to the one below.

reason: 'ERC721: transfer caller is not owner nor approved',
code: 'CALL_EXCEPTION',
method: 'transferFrom(address,address,uint256)',
errorArgs: [ 'ERC721: transfer caller is not owner nor approved' ],
errorName: 'Error',
errorSignature: 'Error(string)',
address: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88',
args: [
'0x66fe4806cD41BcD308c9d2f6815AEf6b2e38f9a3',
'0xC41672E349C3F6dAdf8e4031b6D2d3d09De276f9',
100
],
transaction: {
data: '0x23b872dd00000000000000000000000066fe4806cd41bcd308c9d2f6815aef6b2e38f9a3000000000000000000000000c41672e349c3f6dadf8e4031b6d2d3d09de276f90000000000000000000000000000000000000000000000000000000000000064',
to: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88',
from: '0xC41672E349C3F6dAdf8e4031b6D2d3d09De276f9'
}
}

You can see the reason: ‘ERC721: transfer caller is not owner nor approved’ as the reason why this transaction will fail. The most important thing is that static call is a read-only function and is gasless.

By integrating a static call in your dApp, bots, etc., you will save a lot of money that could have been spent paying gas fees for failed transactions.

If you managed to follow through up till this point, then you must have successfully sent a static call to the Blockchain. Great job!

You can check out the full code below:

PS: This tutorial uses ethers v6.3. It may not be compatible with newer ethers versions

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Jelilat Anofiu
Jelilat Anofiu

Written by Jelilat Anofiu

Blockchain Engineer | Smart Contracts Developer

Responses (4)