Skip to content

Clarification: should frozen ERC20 spenders be blocked from pre-existing infinite allowances? #470

Description

@calc1f4r

Hi Chainlink team,

I'm reviewing the freeze semantics in the CCIP EVM Freezable BurnMint ERC20 contracts and noticed a difference between finite and infinite allowances after a spender is frozen.

Affected contracts:

  • BurnMintERC20PausableFreezableUUPS
  • BurnMintERC20PausableFreezableTransparent

Summary

The contracts block frozen accounts in:

  • _update(from, to, value), when the frozen account is from or to
  • _approve(owner, spender, value, emitEvent), when the frozen account is owner or spender

This means a frozen spender cannot receive a new approval, and finite allowance spends revert because OpenZeppelin's _spendAllowance decreases the allowance through _approve.

However, OpenZeppelin ERC20 v5 skips _approve when the current allowance is type(uint256).max.

As a result, a spender that was approved for infinite allowance before being frozen can still call transferFrom and move third-party funds, as long as neither from nor to is frozen.

Example flow

// 1. Alice approves Spender before Spender is frozen
token.approve(spender, type(uint256).max);

// 2. Spender is later frozen
token.freeze(spender);

// 3. Spender calls transferFrom
token.transferFrom(alice, bob, amount);

Current result:

  • _spendAllowance(alice, spender, amount) does not call _approve because allowance is max.
  • The frozen-spender check in _approve is skipped.
  • _update(alice, bob, amount) only checks alice and bob.
  • If neither alice nor bob is frozen, the transfer succeeds.

For finite allowances, the same flow reverts because _spendAllowance calls _approve, which detects the frozen spender.

Clarification requested

Is this intended behavior?

More specifically, is the intended freeze invariant only:

A frozen account cannot be the from or to address in transfers, mints, or burns.

Or should the invariant also include:

A frozen account cannot act as msg.sender / spender and spend third-party allowances through transferFrom.

The ambiguity comes from _approve rejecting frozen spenders, which suggests frozen spenders are intended to be blocked from allowance-based flows. But infinite allowances bypass that check.

Related burnFrom behavior

The same pattern may apply to burnFrom if the frozen caller still has the required burn role and has a pre-existing infinite allowance, because burnFrom also relies on _spendAllowance.

Possible fix if unintended

If frozen spenders should not be able to spend allowances, the contract could override _spendAllowance and check the spender before OpenZeppelin's infinite-allowance shortcut:

function _spendAllowance(address owner, address spender, uint256 value) internal virtual override {
    if (isFrozen(spender)) {
        revert AccountFrozen(spender);
    }

    super._spendAllowance(owner, spender, value);
}

Exact error names may differ, but the key point is that the spender freeze check should happen before the max-allowance branch.

Can the Chainlink team confirm whether frozen accounts being able to spend pre-existing infinite allowances is intended behavior?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions