Short version: one view function, behaviorFlags, returning a uint256 in which each bit names a way the token departs from plain ERC-20 or ERC-721. Fee-on-transfer, rebasing, transfer hook, pausable, blocklist, non-transferable, upgradeable, mintable, seizable, and two that only mean anything for 721, operator-restricted and metadata-mutable. The function is not the interesting part. Two rules hang off it, one of which I am confident about and one of which I am not, and there is a third problem underneath both that I have not solved and would mostly like help with. The first rule is that a flag is set when the behavior is installed, not when it is currently active. A token whose fee rate is sitting at zero still declares fee-on-transfer, because whoever holds the fee key can raise it tomorrow. The obvious objection is that this makes the word useless, since issuers will install modules they never switch on, everything will declare everything, and integrators will go back to ignoring it. I think that objection is about half right and I do not have a clean answer to it. What the rule buys is that a false negative becomes impossible, and the false negative is the one that costs money. I would rather have a signal people over-read than one they cannot lean on. But this is the part of the design I hold least firmly, and if someone has a better shape I want to hear it. The second rule is the one I actually care about. The word is fixed at deployment. Configuration moves. Declarations do not. That sounds like a small thing and I think it is the whole point, because it turns a call into a fact. Every discovery mechanism I looked at answers per call or per caller, which means an integrator can never stop asking. There is no moment where they are done. Under an immutability rule they read the word once, store it next to the address, and are finished with that question permanently. Everything else in this proposal is downstream of wanting that property. It has one exception, and the exception is doing a lot of work: upgradeable. If the code can be replaced then nothing about it is fixed, so the vocabulary has to name that case itself and callers have to check that bit before trusting any of the others. Which is fine in principle and slightly uncomfortable in practice, because upgradeable proxies are extremely common, and for those tokens the immutability rule collapses back into “as durable as the upgrade authority”. I do not think this sinks it. Most of the tokens where the question matters most are the ones deployed by people who wanted them to be immutable. But I notice I am arguing for the rule using the subset of cases where it works. I got the vocabulary wrong on the first pass, and the way I got it wrong seems worth reporting. My original flags described what a transfer does. Fee, hook, pause, freeze, non-transferable. Which meant a token that installed no extensions returned zero, and I had defined zero as indistinguishable from a plain ERC-20. Except that token could still mint without limit and burn any balance it liked, because mint and burn sit on the base contract and are not optional. It was answering “nothing to see here” while holding two of the powers that matter most to anyone holding it, and neither shows up in a transfer simulation. Ever. I added mintable and seizable. The general version, which I would want written into any spec that comes out of this: a vocabulary assembled only from what calls do will systematically miss what the deployer can do, and those are precisely the properties no simulation will ever surface. One smaller rule, which I would like adopted even if everything else here is wrong. A failed call must not be read as zero. The tempting shape is a try/catch with zero in the catch branch and I am fairly sure that is what most integrations would write. It collapses “declares nothing” into “does nothing”. Most fee-on-transfer tokens have never heard of this interface and their silence denies nothing, and an uninitialised contract reverts today and can be initialised with a fee tomorrow. Unknown has to be its own state. Then there is the objection everyone arrives at, which is that a token can simply lie. ERC-5269 got there first. Its thread raised the objection, concluded that verification fell outside that proposal’s scope, and as far as I can tell nothing since has picked it up. I think it is answerable without touching the interface at all. If canonical tokens are EIP-1167 clones of a shared runtime then the deployed code is 45 bytes, a fixed prologue and an address and a fixed epilogue, with nowhere to hide behavior. One EXTCODECOPY establishes that a contract runs an implementation you already decided to trust. No call to the token, no registry in the loop, and since EIP-6780 the result cannot change afterwards. That does not make declarations trustworthy in general. It moves the trust onto a single address the caller chose, which is a far smaller thing to audit than every token they might meet. The counter that came up there, that access-controlled contracts behave differently depending on who is asking, is correct, and it is an argument about what belongs in the declaration rather than against verification: code identity tells you the rules are the published ones, not that you will like them, and nothing at all about who holds the keys. Which is how I arrived at mintable and seizable from the other direction. The clone check is a pattern, not part of the interface. I am not proposing it for standardisation. Now the part I am actually stuck on. Who assigns bit 11? Bit values get copied into integrator code. That is the entire point, since the value of a bitmask over an interface hierarchy is that reading it is cheap and local. But it means the moment a new bit is assigned, every deployed copy of the vocabulary is stale, and a caller holding an old copy silently drops a behavior it has never heard of. Silently is the problem. Not “reverts”, not “returns unknown”, just quietly treats a token as understood when a bit it cannot name is set. The partial fix is a rule that a caller reading a word containing bits outside its known mask must treat the token as not fully classified, which pushes it into the unknown tier rather than letting it pass. I believe that is necessary. I do not think it is sufficient, because it converts every vocabulary expansion into a compatibility break for every integrator who has not updated, and that is a strong incentive to never expand, which in turn is a strong incentive for implementers to squat unassigned bits with private meanings. My working position is that the ERC text is the registry, new bits arrive only by amendment, unassigned bits stay reserved. I am not confident. ERC-165 avoids this problem entirely by making the identifier derive from the interface, at the cost of the thing I am trying to get. I would genuinely like to be argued out of the bitmask, if someone can see a shape that keeps one cacheable read without a central allocation authority. A smaller version of the same question: my implementation refuses to deploy a token declaring both non-transferable and fee-on-transfer, on the grounds that an unreachable transfer path cannot charge a fee. Having lived with it I suspect that kind of refusal is over-reach for a standard, which should describe rather than forbid, but I have not convinced myself either way. Prior art, briefly, and I am happy to go into any of these properly in replies. ERC-7943 is the one I take most seriously since it is Final and canSend, canReceive and canTransfer already cover restriction, but that is a per-transfer question about specific parties and this is a cached fact about the contract, and I do not think they conflict. ERC-165 gets asked about immediately: it tells you an interface exists rather than whether a behavior is reachable, costs one call per interface so nothing is cacheable as a unit, and supportsInterface lies exactly as easily. ERC-1404 is where detectTransferRestriction comes from and I use its signatures, though it never became a finalised ERC and I would rather say so than let the number imply otherwise. ERC-721C enforces operator policy where this only declares one exists, so a 721C collection setting the bit is more useful to a marketplace rather than less. The Operator Filter Registry being retired in 2023 after everyone routed around it reads to me as evidence that enforcement was the contested part and discoverability never really got tried. Security considerations need a real section in any draft. The two that worry me most: under-declaring is the dangerous direction and the interface cannot prevent it, only make the claim explicit and checkable; and a zero word is not an all-clear, since a token can be perfectly honest about every bit here and still be broken in ways this vocabulary does not name. There is a reference implementation on OpenZeppelin v5 covering both token types, 334 tests, four rounds of internal review that each turned up something the tests had been passing over. No external audit, nothing deployed anywhere, no integrator has touched it. LINK Last thing, and it is the doubt underneath all of the above. I am not sure this should be an ERC. If bit meanings cannot be allocated centrally then what I have is a convention, and conventions do not obviously belong in the EIP process. If they can, then the allocation mechanism is the actual standard and the function signature is trivia. I have been unable to decide which of those I am proposing, and that is probably the most useful thing anyone could tell me. 3 posts - 2 participants Read full topic
This item was indexed from its published source and tied to the entity records it names. The Counterparty reports what the market says and who it says it about; it does not rewrite the original report. Read the source for the full account.