🧪 Simulate ERC20 transfers via state overrides to detect fee-on-transfer tokens.
go get github.com/orbs-network/transfer-simclient, _ := ethclient.Dial("https://eth-mainnet.alchemyapi.io/v2/YOUR-API-KEY")
token := common.HexToAddress("0x...")
from := common.HexToAddress("0x...")
to := common.HexToAddress("0x...")
amount := big.NewInt(1000000000000000000) // 1 token (18 decimals)
received, err := transfersim.TransferSim(client, token, from, to, amount)
if err != nil {
// On error, received == amount. Treat as unknown.
fmt.Printf("Simulation error: %v\n", err)
return
}
if received.Cmp(amount) == 0 {
fmt.Println("No fee on transfer detected")
} else {
fee := new(big.Int).Sub(amount, received)
fmt.Printf("Fee on transfer detected: %s tokens\n", fee.String())
}The Go library is the canonical implementation. A Node.js translation is provided in js/transfer-sim.js for convenience.
const Web3 = require("web3");
const { transferSim } = require("./js/transfer-sim");
const web3 = new Web3(process.env.ETH_RPC_URL);
const token = "0x...";
const from = "0x...";
const to = "0x...";
const amount = 10n ** 18n;
(async () => {
const { received, error } = await transferSim(web3, token, from, to, amount);
if (error) {
console.warn("Simulation error:", error.message || error);
}
console.log("received:", received.toString());
})();- Requires
fromto approvetoto spendamounton-chain. - Requires an RPC that supports
eth_callwith state overrides.
func TransferSim(
client *ethclient.Client,
token, from, to common.Address,
amount *big.Int,
) (*big.Int, error)- Returns the actual amount received by
to(balance delta). - If
amountisnil/0, returns0without calling RPC. - On failure, returns
amountalongside the error. - Requires
fromto approvetoto spendamount. - Uses
eth_callwith state overrides (no on-chain tx).
npm testRequires Node.js, Go, and Foundry (forge and anvil). Forge downloads Solidity
0.8.21 on the first run if needed.
The E2E suite starts a fresh local Anvil node, deploys a token fixture, and runs
the same scenarios through both JS and Go over real JSON-RPC. It executes the
receiver bytecode using state overrides and checks full transfers, 2.5% and 100%
fees, large amounts, insufficient balances, and insufficient allowances. Every
simulation must preserve the original balances, allowances, and receiver code.
Scenarios are defined once in test/e2e.test.js; test/runner only adapts Go to
that shared harness.
Unit tests cover only cases outside E2E: zero amounts without RPC, JS transport errors, and Go panic recovery. No user wallet, external RPC, or live-chain funds are used. The local node and temporary build artifacts are cleaned up after testing.
Run npm run test:e2e for E2E only.
Run npm run build for JS syntax and Go compilation checks.