Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: medium
Likelihood: medium

Missing Approval Event in _spendAllowance() – Violates Widely Adopted ERC20 Best Practice

Author Revealed upon completion

In the ERC20 standard, when tokens are transferred via transferFrom, the spender's allowance should be reduced by the transferred amount. The normal behavior, as implemented by OpenZeppelin and followed by most ERC20 contracts, is to emit an Approval event reflecting the new (reduced) allowance after each successful spend operation.

The specific issue is that the _spendAllowance function reduces the allowance in storage but does not emit the corresponding Approval event. This creates a discrepancy between the on-chain state and the event logs that off-chain systems rely on

Likelihood: HIGH

  • Reason 1: This function will be called every time transferFrom is used with a non-infinite allowance. Since transferFrom is a core ERC20 function used in DeFi protocols, exchanges, and wallet interactions, this missing event will occur in virtually all token transfer scenarios.

  • Reason 2: Any protocol integration that tracks allowances via events will encounter this inconsistency. Given that event-based tracking is standard practice for indexers, explorers, and monitoring tools, this affects the majority of ecosystem tooling.

Impact:

  • Impact 1: Off-chain systems (indexers, block explorers, wallets) will display incorrect allowance values, as they rely on Approval events to update allowance state. Users and integrators will see stale allowance data.

  • Impact 2: DeFi protocols and smart contracts that monitor allowance changes via events may malfunction. For example, a contract that listens for allowance approvals to trigger subsequent operations will not detect allowance decreases from transferFrom calls.

Proof of Concept

// Test scenario showing the missing event
function testMissingApprovalEvent() public {
address owner = address(0x1);
address spender = address(0x2);
uint256 initialAllowance = 1000;
uint256 transferAmount = 100;
// 1. Owner approves spender
vm.prank(owner);
token.approve(spender, initialAllowance);
// Approval event emitted here ✓
// 2. Spender transfers tokens using allowance
vm.prank(spender);
token.transferFrom(owner, address(0x3), transferAmount);
_spendAllowance is called internally
Allowance is reduced to 900 in storage
BUT: No Approval event is emitted for the new allowance of 900 ✗
}

Recommended Mitigation

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
assembly ("memory-safe") {
let ptr := mload(0x40)
let baseSlot := _allowances.slot
// ... (existing assembly code to calculate allowanceSlot and currentAllowance)
if lt(currentAllowance, value) {
// ... (existing revert logic)
}
let newAllowance := sub(currentAllowance, value)
sstore(allowanceSlot, newAllowance)
mstore(0x00, value)
mstore(0x20, newAllowance)
log3(
0x00,
0x40,
0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925,
owner,
spender
)
}
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!