Allowance-Axis Confusion / Approval Synthesis

  • Name: allowance-axis confusion / approval synthesis
  • Category: exploit pattern / authorization-state corruption / owner-spender axis inversion / approval synthesis
  • Summary: These exploits happen when a protocol stores delegated spend authority in an ordered relation such as allowance[owner][spender], but one code path reads, writes, or updates that relation under the wrong principal ordering. The attacker first seeds an approval on the axis they can control, then triggers a helper such as burnFrom(), transferFrom(), or another delegated-spend path that interprets that seeded approval as if it came from the victim in the opposite direction. The bug does not merely misuse an existing approval. It can synthesize a brand-new victim-to-attacker spend right out of an attacker-to-victim approval or another wrongly oriented authorization slot.
  • What it is:
    • Authorization state is keyed by an ordered principal pair, not just by one address.
    • The code path that consumes or updates that state silently swaps those roles at some point.
    • An attacker can pre-seed the controllable orientation of the relation, then make the victim-facing path read that seeded state as if it represented victim consent.
    • The helper often finishes by writing the authorization back under the opposite orientation, turning attacker-controlled state into victim-granted spend power.
    • Once the forged approval exists, an ordinary transferFrom() or sibling delegated-spend path cashes out real user balances.
  • Typical preconditions:
    • The protocol keeps delegated-spend or delegated-burn authority in a mapping or state object whose key order matters.
    • A public helper such as burnFrom() or transferFrom() both reads and mutates that authorization state.
    • The attacker can first create or influence the authorization entry on one axis, typically by approving the victim or another attacker-chosen address from their own wallet.
    • The vulnerable helper accepts a victim-controlled account parameter and trusts the wrong allowance slot when reconciling delegated spend.
    • A zero-amount or low-amount call is enough to pass through the helper and crystallize the forged approval without needing meaningful victim cooperation.
  • Common vulnerable surfaces:
    • custom ERC-20 burnFrom() implementations forked from OpenZeppelin or older burnable-token patterns
    • delegated-spend helpers that read currentAllowance from one principal order and write back with the other
    • operator / delegate approval mappings where the code forgets which address is the principal and which is the actor
    • token upgrades or emergency patches that rewrite familiar allowance logic by hand instead of reusing battle-tested library code
    • zero-amount delegated verbs whose accounting still updates authorization state
  • Exploit sequence:
    1. The attacker finds a delegated-spend or delegated-burn path that reads authorization from the wrong axis.
    2. They seed the authorization slot they can control, such as allowance[attacker][victim].
    3. They call the vulnerable helper with the victim supplied as the nominal account / owner argument.
    4. The helper reads the attacker-seeded slot as if it were proof that the victim approved the attacker.
    5. The helper then writes back or preserves authorization under the victim-facing axis, synthesizing a real victim-to-attacker approval.
    6. The attacker uses an ordinary delegated-transfer path to pull the victim’s real tokens.
  • Variants:
    • Read/write axis inversion — the function reads one principal ordering and writes the opposite ordering back to state.
    • Zero-amount approval synthesis — a helper like burnFrom(account, 0) performs the dangerous allowance update even though no meaningful spend happened.
    • Delegated-burn seam — a burn helper intended to consume spend authority becomes the primitive that manufactures it.
    • Owner/spender identity drift — the bug is not arithmetic but role confusion about who is supposed to have authorized whom.
  • Detection heuristics:
    • For every authorization mapping, write down the exact semantic meaning of each key position and check every read and write against that meaning.
    • Add invariants that any helper consuming delegated authority may only reduce the exact (owner, spender) relation it claims to consume.
    • Fuzz zero-amount delegated verbs aggressively; amount == 0 often strips away business-logic friction and exposes pure authorization-state corruption.
    • Differential-test custom token logic against the upstream OpenZeppelin implementation or another known-correct reference.
    • Review upgrades and emergency patches for hand-written copies of familiar allowance logic; these bugs often enter through small local edits to standard code.
  • Defenses:
    • Reuse battle-tested library implementations for delegated spend and delegated burn instead of open-coding owner/spender reconciliation.
    • Name principal roles explicitly in code and review notes; account and msg.sender() are not enough when ordered authorization state is involved.
    • Add property tests asserting that no call can increase or synthesize third-party approval as a side effect of consuming approval.
    • Treat zero-amount delegated operations as first-class test cases, not harmless edge conditions.
    • Where possible, separate authorization consumption from unrelated side effects so a failed or no-op economic action cannot still rewrite spend rights.

Three allowance-truth promotions reviewers should not collapse

1. Some ordered authorization slot exists

An entry like allowance[x][y] being nonzero only proves that one concrete ordered relation was stored somewhere. It does not yet prove that x is the real owner the later spend path means, that y is the real spender now attempting to act, or that a helper may reinterpret the same bits under a different principal ordering.

2. That slot still counts as victim-granted delegated-spend truth

The dangerous promotion happens when a helper such as burnFrom() or transferFrom() silently treats attacker-seeded state like allowance[attacker][victim] as if it were canonical proof of allowance[victim][attacker]. Healthy code keeps stored slot exists separate from this slot is the exact owner/spender truth this path may consume.

3. That accepted relation still deserves live token-pull authority

Even after the protocol identifies one legitimate (owner, spender) pair, the remaining question is whether this branch should get to spend against it now, especially on zero-amount, delegated-burn, no-op, or helper-only paths. Healthy design keeps approval relation is real separate from this exact helper may cash it out or mutate it right now.

What sound design looks like

  • One ordered authorization slot should preserve one meaning everywhere. If the token stores delegated-spend truth as (owner, spender), every helper that reads, consumes, or rewrites that state should talk about the same ordered pair rather than reconstructing meaning from loose variables like account, from, or msg.sender() mid-function. Spender authority truth as a single security object is the broader positive-design companion for keeping that approval object fused before any delegated path reaches transferFrom() or burnFrom().
  • Healthy delegated-spend code keeps three facts separate: some ordered slot is populated, that slot is the exact victim-granted owner/spender relation this path may trust, and this exact helper branch may spend or mutate against that relation right now. Allowance-axis exploits collapse those steps into one vague allowance exists story.
  • Delegated verbs should be monotone consumers of authority, not authors of new authority. burnFrom(), transferFrom(), permit settlement, operator pulls, and sibling helpers should only narrow the exact approval relation they were handed; they should never be able to rotate it, mirror it, or crystallize it onto a different principal pair as a side effect.
  • Principal naming should make role inversion hard to write. Healthy token code tends to carry explicit owner, spender, source, authorizedCaller, and recipient semantics through helper boundaries, events, and tests so reviewers are not forced to infer orientation from storage indices after the fact.
  • Zero-value and no-op branches should preserve the same authorization invariant as value-moving branches. A call that spends 0 should either be a strict no-op on approval state or consume the exact same ordered relation it claims to consume; it should never become a free state-rewriting lane.
  • Customizations to familiar allowance code should shrink, not expand, semantic freedom. If a protocol really must fork standard delegated-spend logic, the surrounding design should compensate with tighter invariants, explicit role typing, and differential tests that prove no attacker-controlled (attacker, victim) approval can ever emerge as live (victim, attacker) authority.
  • The clean design question is: if a later transferFrom(victim, attacker, amount) succeeds, where exactly did the system prove that the victim authorized that exact spender relation rather than the protocol manufacturing it from some differently oriented slot?

Failure-prevention checklist

  • Treat every ordered authorization mapping as a typed security object, not just a nested hash map: write down in code comments and review notes which slot is owner, which is spender, and which verbs are allowed to mutate that relation.
  • Test the three promotions independently: proving one ordered slot is populated must not automatically prove it is the victim-facing relation the current path means, and proving the relation is real must not automatically prove this helper branch may spend or rewrite it.
  • Keep delegated-spend helpers one-directional. A function that consumes (owner, spender) authority should only ever read and decrease that exact slot, never infer or rewrite the opposite orientation as a convenience step.
  • Assert monotonicity on authorization state: delegated-spend and delegated-burn paths should not be able to increase allowance, create fresh third-party approval, or move approval onto a different principal pair as a side effect.
  • Make zero-amount paths obey the same authorization invariants as nonzero paths. amount == 0 should not become a free pass through code that still updates allowance state.
  • Prefer narrow helper boundaries so burnFrom(), transferFrom(), and sibling delegated verbs do one job each. The more accounting and authorization reconciliation a helper performs at once, the easier it is to swap principal meaning mid-function.
  • Differential-test any custom or upgraded allowance logic against a known-correct reference implementation, especially after emergency patches or token migrations.
  • If custom semantics are unavoidable, expose principal names in variable naming, events, and invariants. Reviewers should not have to reverse-engineer whether account, sender, operator, or from is the true allowance owner.
  • Add invariant tests around all public delegated verbs proving that an attacker cannot start from allowance[attacker][victim] and end with usable allowance[victim][attacker] authority.
  • Treat delegated-burn paths as spender surfaces, not as harmless supply-reduction helpers. Any path that consults allowance can become an approval-manufacturing seam if role ordering drifts.

Audit prompts

  • For each authorization mapping, what exact semantic meaning does each key position carry, and does every read and write preserve that meaning without exception?
  • Where does the code separately prove some slot exists, this slot is the exact owner/spender relation this helper intends to trust, and this helper branch may still spend or mutate against that relation now?
  • Can any delegated helper read allowance under one axis and write it back under another, even on revert-adjacent, zero-amount, or no-op branches?
  • Does any helper consume approval and also perform unrelated bookkeeping that could accidentally crystallize a forged authorization relation?
  • If burnFrom(account, 0) or an equivalent zero-value delegated verb is called repeatedly, can it still mutate approval state or leave behind a spendable victim-facing allowance?
  • Which public entrypoints can touch delegated authorization state, and do they all enforce the same owner/spender interpretation as the canonical transfer path?
  • Was any standard-library allowance logic forked, rewritten during an upgrade, or locally optimized? If so, what differential tests prove it remained directionally correct?
  • Are variable names, events, and comments precise enough that a reviewer can tell who is authorizing whom without mentally simulating storage indices?
  • Could a helper intended to reduce or consume approval ever increase net attacker authority because of underflow protection, max-allowance shortcuts, or mirrored writeback logic?
  • If the token supports delegates, operators, permits, or meta-transactions, are those authorization relations kept disjoint from plain allowance slots, or can role confusion bleed across them?
  • What invariant or runtime assertion would fire first if an attacker tried to transform self-seeded approval into victim-granted approval through a delegated helper?

Lessons that should already be standard

  • Direction is part of authorization. allowance[A][B] and allowance[B][A] are different security facts, not symmetric bookkeeping views of the same relationship.

  • Allowance slot exists, slot means victim consent, and this branch may spend against it now are three separate approvals. Collapsing them is how protocols synthesize spend rights out of state the victim never granted.

  • Consuming approval should never be able to manufacture approval. If a helper that spends authority can also create or rotate it, the protocol has fused two trust boundaries that should stay separate.

  • Zero-amount calls are often the cleanest way to expose pure authorization corruption because they strip away economic noise and leave only state semantics.

  • Hand-written rewrites of familiar allowance logic are high-risk even when they look trivial. The most catastrophic bugs here often come from one swapped principal in otherwise ordinary code.

  • Delegated-burn helpers deserve the same scrutiny as delegated-transfer helpers. Destroying tokens still spends somebody’s rights if allowance gates the path.

  • Reviewers should be able to point at one exact state relation and say this user authorized this spender for this scope. If the code allows that story to flip orientation mid-function, the protocol is already manufacturing false consent.

  • Token upgrades and emergency patches are dangerous times for authorization drift because teams feel they are editing boring standard logic instead of a live trust boundary.

  • Property tests for authorization monotonicity are not optional polish. They are often the only reliable way to catch approval synthesis before mainnet users do.

  • Approval bugs are not always about broad infinite allowances. Sometimes the protocol itself forges the dangerous allowance out of a relation the victim never granted.

  • Where this pattern stops:

    • Use exposed-token-pull-helper-approval-rights-exposure when the protocol already had legitimate spender rights and merely forgot to prove which caller may trigger the spend. Bancor and Beanstalk live there first: the approval already existed and the bug exposed the spender helper.
    • Stay here when the sharper lesson is that the protocol manufactured victim approval by confusing the owner/spender axis inside its own authorization state.
    • Use arbitrary-call-token-approval-drain when the spender already has approval and the exploit needs attacker-routed calldata or generic execution freedom rather than authorization-state corruption.
    • Use account-confusion-root-of-trust-substitution when the protocol first trusted the wrong root contract or account to define who the payer or authority even was. That family is about counterfeit roots; this family is about a real authorization relation being read under the wrong ordering.
  • High-signal incidents:

    • [[../../../incidents/approvals-wallets-integrations/deus-dao-dei-burnfrom-allowance-axis-inversion-approval-synthesis-drain-2023-05-05|DEUS DAO DEI burnFrom() allowance-axis inversion / approval-synthesis drain (2023-05-05)]] — DEUS upgraded DEI with a misordered burnFrom() allowance read, letting attackers pre-approve victims from their own wallets, call burnFrom(victim, 0), synthesize victim-to-attacker approval, and drain about $6.5M across Arbitrum, BSC, and Ethereum.

Internal linkages

Control surface