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

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.

- 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 andabi
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