All incidents

DEI burnFrom Allowance Inversion

Share
May 05, 2023 17:52 UTCAttackLoss: 5,047,470.47 USDCPending manual check1 exploit txWindow: Atomic
Estimated Impact
5,047,470.47 USDC
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
May 05, 2023 17:52 UTC → May 05, 2023 17:52 UTC

Exploit Transactions

TX 1Arbitrum
0xb1141785b7b94eb37c39c37f0272744c6e79ca1517529fec3f4af59d4c3c37ef
May 05, 2023 17:52 UTCExplorer

Victim Addresses

0xde1e704dae0b4051e80dabb26ab6ad6c12262da0Arbitrum
0x7dc406b9b904a52d10e19e848521bba2de74888bArbitrum

Loss Breakdown

5,047,470.47USDC

Similar Incidents

Root Cause Analysis

DEI burnFrom Allowance Inversion

1. Incident Overview TL;DR

At Arbitrum block 87626026, transaction 0xb1141785b7b94eb37c39c37f0272744c6e79ca1517529fec3f4af59d4c3c37ef let an unprivileged attacker drain the DEI/USDC stable pair at 0x7DC406b9B904a52D10E19E848521BbA2dE74888b. The attacker used the broken DEI token implementation at 0xDE1E704dae0B4051e80DAbB26ab6ad6c12262DA0 to forge a pair -> attacker allowance, removed almost all DEI from the pair, synced the pair to a 1 DEI / 5,047,470.472573 USDC reserve state, and swapped the restored DEI back into the pair for 5,047,470.472572 USDC.

The root cause is DEI's reversed allowance lookup in burnFrom. Instead of checking allowance(account, caller), the implementation reads allowance(caller, account) and writes that value back as allowance(account, caller). That lets any caller convert an approval they control into approval over an arbitrary victim's DEI balance.

2. Key Background

DEI on Arbitrum is a proxied token, but the verified implementation code matches the behavior observed in the incident trace. The affected pair is a public DEI/USDC stable pair whose sync() function updates reserves to the pair's current balances without any access control, and whose getAmountOut() and swap() functions price trades from stored reserves.

Those pair mechanics matter because reserve manipulation is the monetization step, not the initial bug. Once the attacker can steal almost all DEI from the pair, calling sync() commits the pair to a near-zero DEI reserve while the USDC reserve remains intact. When the attacker then sends the stolen DEI back and calls swap(), the pair prices the incoming DEI against the collapsed reserve and releases essentially all USDC.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK, not a pure MEV opportunity. The safety invariant for a burn-on-behalf function is: only allowance(account, caller) may authorize burnFrom(account, amount), and the function must decrement that same allowance edge. DEI violates that invariant in its implementation of burnFrom.

In the verified DEI implementation, the function reads _allowances[_msgSender()][account] and then writes that value back with _approve(account, _msgSender(), currentAllowance - amount). For amount = 0, the caller loses nothing, but the victim gains a forged approval toward the caller. That zero-amount state change should never be able to mutate third-party authorization.

The public pair contract then provides deterministic extraction. After the forged approval exists, the attacker can transferFrom almost all DEI out of the pair, leave 1 unit so the pair stays active, call sync() to lock in the manipulated reserve ratio, and finally swap the DEI back for USDC. The trace and balance diff show that exact path and confirm the realized loss.

4. Detailed Root Cause Analysis

Immediately before the exploit transaction, the DEI/USDC pair held 4602837090538811392635120 DEI and 5047470472573 USDC. The relevant victim-side code from the verified DEI implementation is:

function burnFrom(address account, uint256 amount) public virtual {
    uint256 currentAllowance = _allowances[_msgSender()][account];
    _approve(account, _msgSender(), currentAllowance - amount);
    _burn(account, amount);
}

Origin: verified DEI implementation used by the proxy at incident time.

The pair-side monetization primitives are equally direct:

function sync() external lock {
    _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}

function getAmountOut(uint amountIn, address tokenIn) external view returns (uint) {
    (uint _reserve0, uint _reserve1) = (reserve0, reserve1);
    amountIn -= amountIn * PairFactory(factory).getFee(stable) / 10000;
    return _getAmountOut(amountIn, tokenIn, _reserve0, _reserve1);
}

Origin: verified DEI/USDC pair source used in the exploit path.

The on-chain trace shows the invariant break and the monetization sequence in one transaction:

DEI::approve(pair, max)
DEI::burnFrom(pair, 0)
emit Approval(owner: pair, spender: helper, value: max)
DEI::transferFrom(pair, helper, 4602837090538811392635119)
pair::sync()
emit Sync(: 1, : 5047470472573)
pair::swap(0, 5047470472572, attacker_eoa, 0x)

Origin: incident transaction trace around the exploit core.

This proves the exploit does not depend on privileged keys, private order flow, or a special attacker contract. The sender EOA 0x189cf534de3097c08b6beaf6eb2b9179dab122d1 called its own nonce-0 helper at 0xe2ee6252509382a2b6504d5a5f7a1c5018a38168, but that helper only batches public calls. The ACT conditions are therefore straightforward: the victim must hold DEI, the attacker must first set approve(victim, x) on their own DEI balance, and burnFrom(victim, 0) must remain callable so the forged allowance(victim, attacker) can be created without burning any victim tokens.

5. Adversary Flow Analysis

  1. The attacker EOA 0x189cf534de3097c08b6beaf6eb2b9179dab122d1 sent transaction 0xb1141785b7b94eb37c39c37f0272744c6e79ca1517529fec3f4af59d4c3c37ef to helper contract 0xe2ee6252509382a2b6504d5a5f7a1c5018a38168.
  2. The helper called DEI.approve(pair, type(uint256).max), creating an attacker-controlled allowance(helper, pair).
  3. The helper called DEI.burnFrom(pair, 0). Because burnFrom reads the allowance in the wrong direction, it emitted Approval(owner: pair, spender: helper, value: max) and forged a pair -> helper approval without burning DEI.
  4. Using that forged approval, the helper called DEI.transferFrom(pair, helper, 4602837090538811392635119), leaving exactly 1 DEI in the pair.
  5. The helper called pair.sync(), causing the pair to store reserves of 1 DEI and 5047470472573 USDC.
  6. The helper computed getAmountOut on the stolen DEI, transferred the DEI back into the pair, and called pair.swap(0, 5047470472572, attacker_eoa, "").
  7. The pair transferred 5047470472572 raw USDC units to the attacker EOA, leaving the pair with 1 raw USDC unit. The balance diff confirms the attacker EOA ended the transaction with all drained USDC and only paid 407531400000000 wei in native gas.

6. Impact & Losses

The realized loss was the pair's USDC inventory. The balance diff shows the DEI/USDC pair lost 5047470472572 raw USDC units and the attacker EOA gained the same amount.

{
  "token_symbol": "USDC",
  "amount": "5047470472572",
  "decimal": 6
}

That is 5,047,470.472572 USDC. The pair's DEI balance also moved during the exploit path, and DEI fees were distributed to fee recipients during the final swap, but the primary externally realized loss was the drained USDC.

7. References

  • Incident transaction: 0xb1141785b7b94eb37c39c37f0272744c6e79ca1517529fec3f4af59d4c3c37ef
  • Attacker EOA: 0x189cf534de3097c08b6beaf6eb2b9179dab122d1
  • Attacker helper: 0xe2ee6252509382a2b6504d5a5f7a1c5018a38168
  • DEI token proxy: 0xDE1E704dae0B4051e80DAbB26ab6ad6c12262DA0
  • DEI/USDC pair: 0x7DC406b9B904a52D10E19E848521BbA2dE74888b
  • Verified DEI implementation artifact: LERC20Upgradable.sol
  • Verified pair artifact: Pair.sol
  • Supporting evidence: incident metadata, opcode-level trace, and balance-diff artifacts collected for the seed transaction