Token-0x

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

[M-03] Missing Approval Event in _spendAllowance Function (Non-Compliant with ERC20 Standard)

Author Revealed upon completion

Description

  • The _spendAllowance function does not emit the required Approval event when decreasing the spender's allowance. According to the ERC20 standard, any change in allowance must emit an Approval event. Without this event, external observers cannot track allowance changes, breaking interoperability with wallets, block explorers, and other smart contracts that rely on these events for allowance tracking.


Root Cause

The function uses Yul assembly for gas optimization but omits the emission of the Approval event after updating the allowance storage.

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
assembly ("memory-safe") {
let ptr := mload(0x40)
let baseSlot := _allowances.slot
mstore(ptr, owner)
mstore(add(ptr, 0x20), baseSlot)
let initialHash := keccak256(ptr, 0x40)
mstore(ptr, spender)
mstore(add(ptr, 0x20), initialHash)
let allowanceSlot := keccak256(ptr, 0x40)
let currentAllowance := sload(allowanceSlot)
if lt(currentAllowance, value) {
mstore(0x00, shl(224, 0xfb8f41b2))
mstore(add(0x00, 4), spender)
mstore(add(0x00, 0x24), currentAllowance)
mstore(add(0x00, 0x44), value)
revert(0, 0x64)
}
sstore(allowanceSlot, sub(currentAllowance, value))
// <@ Missing Approval event emission: Approval(owner, spender, newAllowance)
}

Risk

Likelihood:

  • Every transferFrom operation (which calls _spendAllowance) fails to emit the required Approval event, affecting all allowance decreases.

  • The absence of events is immediately apparent to any external observer or tool that monitors allowance changes.

Impact:

  • Broken ERC20 Compliance: The token does not fully comply with the ERC20 standard, which may cause integration issues with wallets, DEXs, and other DeFi protocols.

  • Lack of Transparency: External parties cannot track allowance changes, leading to a loss of transparency and auditability for delegated spending.

  • Interoperability Issues: Smart contracts that rely on Approval events to react to allowance changes (e.g., allowance trackers, security monitors) will not function correctly.

  • User Experience Degradation: Wallets and interfaces that display allowance information may show outdated or incorrect values due to missing events.

Proof of Concept

Recommended Mitigation

Emit an Approval event with the new allowance value after decreasing it in _spendAllowance

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
assembly ("memory-safe") {
let ptr := mload(0x40)
let baseSlot := _allowances.slot
mstore(ptr, owner)
mstore(add(ptr, 0x20), baseSlot)
let initialHash := keccak256(ptr, 0x40)
mstore(ptr, spender)
mstore(add(ptr, 0x20), initialHash)
let allowanceSlot := keccak256(ptr, 0x40)
let currentAllowance := sload(allowanceSlot)
if lt(currentAllowance, value) {
mstore(0x00, shl(224, 0xfb8f41b2))
mstore(add(0x00, 4), spender)
mstore(add(0x00, 0x24), currentAllowance)
mstore(add(0x00, 0x44), value)
revert(0, 0x64)
}
sstore(allowanceSlot, sub(currentAllowance, value))
+ let newAllowance := sub(currentAllowance, value)
+ mstore(0x00, newAllowance) // Store new allowance in memory
+ log3(
+ 0x00, // data offset
+ 0x20, // data size (32 bytes for uint256)
+ 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, // Approval event signature
+ owner, // from = owner
+ spender // to = spender
+ )
}

Support

FAQs

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

Give us feedback!