Slash Gas Costs: Off-Chain Computation with ECDSA
In 2022, while building the investment aggregator for Cabala Labs, I hit a wall.
We were building a strategy that required complex mathematical logic to determine an optimal trade route. Coding this logic directly into a Solidity smart contract was possible, but the gas costs were astronomical.
On a busy day, a single user transaction was estimated to cost 0.15 ETH. At the time, that was nearly $400.
You cannot build a consumer product with $400 fees. The solution wasn’t to optimize the Solidity code—it was to remove it entirely.
The “Chequebook” Pattern
The most efficient line of code is the one you never write.
Instead of forcing the Ethereum Virtual Machine (EVM) to calculate the trade route, we moved that calculation to a Node.js backend. The backend does the heavy lifting (for free), signs the result with a private key, and gives the user a “cryptographic coupon” (signature).
The smart contract doesn’t need to know how we got the result. It just needs to verify that I was the one who sent it.
See the Difference
I built this simulator to visualize the difference between running logic On-Chain vs. Off-Chain. Try adjusting the Gas Price (Gwei) to see how the savings scale.
$ Gas Cost Simulator
How It Works: The Math
We use ECDSA (Elliptic Curve Digital Signature Algorithm). The core flow is:
- Off-Chain (Node.js): Hash the data + Sign it with a private wallet (The “Admin”).
- On-Chain (Solidity): Receive the data + the signature. Use
ecrecoverto ensure the signer matches the Admin wallet.
1. The Solidity Verifier
Here is the exact modifier pattern I used to secure the contract. It costs roughly 3,000 gas to execute—peanuts compared to the execution logic.
// The hash includes the specific parameters and a nonce to prevent replay attacks
function getEthSignedMessageHash(bytes32 _messageHash)
public pure returns (bytes32)
{
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
function verify(
address _signer,
string memory _message,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = keccak256(abi.encodePacked(_message));
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
return recoverSigner(ethSignedMessageHash, signature) == _signer;
}
2. The Backend Signer (Ethers.js)
On the backend, we act as the “Oracle.”
import { ethers } from "ethers";
const signTrade = async (tradeData: any) => {
const wallet = new ethers.Wallet(process.env.ADMIN_PRIVATE_KEY);
// Pack the data exactly how Solidity expects it
const messageHash = ethers.utils.solidityKeccak256(
["uint256", "address", "uint256"],
[tradeData.amount, tradeData.user, tradeData.nonce],
);
// Sign the binary data
const signature = await wallet.signMessage(ethers.utils.arrayify(messageHash));
return signature;
};
The Impact
By implementing this pattern at Cabala Labs:
- We reduced average gas costs by ~90%.
- We enabled complex arbitrage strategies that were previously economically impossible.
- We maintained security: the contract would revert instantly if a user tried to spoof the trade parameters without a valid signature from our backend.
In Web3, optimization isn’t just about clean code—it’s about knowing what code doesn’t belong on the blockchain at all.