betterERC
All posts
DesignAugust 6, 20268 min read

Finding out by losing money

There are two ways to learn how a token behaves before you integrate it. One assumes the verified source is what is deployed. The other is a sample of size one.

An integrator deciding whether to list a token has two options today, and both are bad in a way that is easy to miss.

The first is to read the source. That assumes the source is verified, that it is readable, and that what you are reading is what actually sits behind the address — three assumptions that hold most of the time and fail exactly when it matters. It also assumes you will read it again after every configuration change, which nobody does.

The second is to simulate a transfer and see what arrives. That buys you one true fact about one sender, one recipient and one amount. Fees are capped, exemptions are per-account, freezes are per-account, and a hook can branch on anything it likes. A sample of size one generalises to nothing.

The problem with fee-on-transfer tokens was never the fee. It was that you had to find out by losing money.

What a word buys

The alternative is that the token says so. One word, fixed at deployment, with one bit for each way the token departs from what a plain ERC-20 would do: value withheld on transfer, balances that move without a Transfer event naming the account, a call into another contract, a global pause, a per-account freeze, transfers that never work at all, and code that can be replaced.

Read it once. Cache it beside the address. Branch on it. There is no second call, no simulation and no source to trust, because the word is a commitment the token made about itself in public and cannot quietly take back.

Router.sol
uint256 flags = IERC20Behavior(token).behaviorFlags();

if (flags & BehaviorFlags.FEE_ON_TRANSFER != 0) {
    // Quote against what will arrive, not what you send.
}

The tokens that have never heard of it

Most of the supply on chain today predates all of this and will never be redeployed, so the interesting case is not what a declaring token says. It is what a non-declaring one does. It has no such function, and the call reverts.

The tempting move at that point is to catch the revert and carry on with zero. It compiles, it never throws, and it is the single most expensive line an integrator can write against this framework. Zero is a statement — this token declares nothing, and here is the code that says so. A revert is the absence of a statement. Collapsing the second into the first turns "I could not classify this token" into "this token is ordinary", and plenty of ERC-20s take a cut of every transfer without having heard of any of this.

Silence is not a denial. A token that says nothing has not told you it does nothing.

So a revert maps to unknown, and unknown means whatever policy you already apply to arbitrary tokens — which you have, because every token was in that category until this framework existed. That is the entire adoption story and it is deliberate. A standard that needs the world to move first is not a standard, it is a wish. This one degrades to the status quo on the first token that has not adopted it, which is the only way a standard ever gets adopted at all.

And then the harder question

Everything above is about a token that speaks. It leaves the second question untouched: a word is only worth the code that produced it, and a token declaring nothing while charging two percent is just a token that lied. Reading the declaration moves an integrator from no information to a claim. Getting from a claim to a guarantee takes a different mechanism entirely, and it is the subject of its own post.