TL;DR: The popular JavaScript library Ethers v5 can overpay FVM smart contract calls by 6000x. A single contract call can cost >2FIL instead of negligible 0.0003 FIL.
Ethers is a popular JavaScript framework for interacting with Ethereum. Due to Ethereum’s popularity, other blockchains follow its implementation. Therefore, Ethers is also frequently used with other Ethereum compatible blockchains.
On March 14th 2023, the FVM added programmability to the Filecoin blockchain - with EVM support out of the box. This means that you can mostly deploy smart contracts written for Ethereum to Filecoin - unchanged - and also use a lot of its tooling. Since there exists so much more tooling for Ethereum than for Filecoin, this is very important.
One such tool is the Ethers JavaScript library, which is one of the easiest ways of interacting with blockchains and smart contracts programmatically. Since the Station applications are predominantly written in JS, we also started using it.
However, there was a big problem: When the Station Network suddenly grew very quickly at the start of 2024, with Ethers v5, we were heavily overpaying on gas, leading to spending thousands of US dollars on interacting with the chain. The Meridian services are now performing millions of smart contract calls every month, so overspending on gas came in at a heavy weight.
After we were able to diagnose and upgrade to Ethers v6, this went down to basically 0 spending. To make sure no one else repeats this mistake, we want to share this story.
JSON-RPC node incompatibility
At the point when we started investigating our gas spending, only Ethers version 5 (and possibly prior) was compatible with Filecoin. The current version 6 didn’t work with the publicly available JSON-RPC providers. This is for two reasons.
Batching
JSON-RPC providers didn’t yet support a features that Ethers version 6 is configured to use by default: Batching.
6 Batch To send several Request objects at the same time, the Client MAY send an Array filled with Request objects. The Server should respond with an Array containing the corresponding Response objects, after all of the batch Request objects have been processed. A Response object SHOULD exist for each Request object, except that there SHOULD NOT be any Response objects for notifications. The Server MAY process a batch rpc call as a set of concurrent tasks, processing them in any order and with any width of parallelism. […]
At that time, we weren’t aware that request batching was why Ethers v6 wasn’t working, so we continued using Ethers v5.
const provider = new ethers.JsonRpcProvider(
fetchRequest,
null,
{ batchMaxCount: 1 }
)
eth_getFilterChanges
The other method not supported at that time was eth_getFilterChanges
. See this GitHub issue for more details. It is another method that Ethers v6 started using, only this one can’t be disabled by reconfiguring the library. eth_getFilterChanges
is used for receiving new events emitted by the smart contract. We were stuck once more, at least on the services that are listening for events.
Then, after talking to the awesome people at Glif, eth_getFilterChanges
was finally patched in lotus gateway code, and now should work as expected (our services are still using the above mentioned library).
Ethers v5 gas calculation
The problem when you’re stuck on version 5 of Ethers, is that it sets a constant gas premium of 1.5 nFIL:
There is a harcoded (1.5 gwei) maxPriorityFeePerGas (and maxFeePerGas) in index.ts.
We weren’t experienced enough with smart contract development at that point, to notice that this gas cost is unusual. We were noticing that smart contract calls are expensive (2+ FIL), but because this is on a L1 and we know that Ethereum L1 calls are expensive, it didn’t raise a red flag.
We therefore looked at optimizing the contract, as with 2+ FIL per transaction our system was just too expensive to operate, and it seemed the obvious next place to look.
A long tale of Solidity gas optimization
The foundry toolkit supports Gas Snapshots, which lets you quickly see the impact of your code changes on the gas consumption of your test files. Using negative diffs as justification, we landed a bunch of gas improvements.
Most notable optimizations:
- save gas using
calldata
(-7%) - make variable
constant
(-7%) - use
uint
instead ofuint64
(-2%) - inline functions (-2%)
- batch state updates (-20%)
While this led to an overall gas reduction of 34%, the contract was still too expensive to run sustainably. We would have needed something like a 99.9% cost reduction, so this was still not the answer.
The community to the rescue
In this thread on Filecoin Slack, unrelated to gas overspending, community member and miner f8-ptrk
(Patrick) noticed that we have a constant 1.5nFIL gas premium, and that is…
…way too high
This feedback was crucial for us discovering that Ethers v5 sets a constant gas premium, and that that led to our massive overspending. Being a miner, Patrick would actually profit from Station overspending on gas like this.
Huge kudos to f8-ptrk
on Filecoin Slack! 🙌
Ethers v6 to the rescue
We then tried Ethers version 6 in another project, and fortunately noticed that it manages to create smart contract calls with significantly lower gas spending.
This might be because it uses Polygon gas stations. We’re not sure if this works on FVM, so the gas reduction might be because of another change in Ethers v6. More research is necessary here. For now, we’re happy that it just works.
Time to migrate
Once the JSON-RPC providers had been updated, the only work left was upgrading all our code from Ethers v5 to Ethers v6. We followed their migration guide, which was all we needed at this point.
Now we could finally sit back and watch our Meridian platform operate at a very comfortable (read: unnoticable) gas price, right on the Filecoin L1 chain.
👋 Thank you for reading. This was our journey with smart contracts, gas optimization and Ethereum tooling on the FVM.