52925 sc medium usdt like approval hygiene can block subsequent operations after partial fill leaves non zero allowance

  • Submitted on: Aug 14th 2025 at 10:49:21 UTC by @LoopGhost007 for Attackathon | Plume Network

  • Report ID: #52925

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/immunefi-team/attackathon-plume-network-nucleus-boring-vault/blob/main/src/helper/DexAggregatorWrapperWithPredicateProxy.sol

Impacts:

  • Smart contract unable to operate due to lack of token funds

  • Temporary freezing of funds for at least 24 hours

Description

Brief / Intro

A medium-severity vulnerability was identified in DexAggregatorWrapperWithPredicateProxy related to allowance hygiene for non-standard ERC20 tokens (USDT-like). Some tokens refuse to change a spender allowance from a non-zero value directly to another non-zero value (they require approve(0) first). The wrapper uses safeApprove(spender, amount) without zeroing a prior non-zero allowance, so a partial fill that leaves a non-zero residual allowance can cause subsequent calls to revert on USDT-like tokens.

The PoC demonstrates that after a partial fill the wrapper left a large non-zero allowance to the router. On the next interaction, USDT-like tokens would revert when attempting a non-zero -> non-zero approve.

Vulnerability Details

Affected helper sites in DexAggregatorWrapperWithPredicateProxy.sol:

  • _oneInchHelper: calls safeApprove(aggregator, amount) without first zeroing allowance

  • _okxHelper: calls safeApprove(okxApprover, amount) without first zeroing allowance

  • Vault approvals: approvals to the vault do not follow a zero-first pattern

Behavior that causes failure:

  • A partial fill leaves a non-zero residual allowance to the router/approver.

  • USDT-like tokens revert when moving directly from a non-zero allowance to another non-zero allowance unless approve(0) is called first.

  • The wrapper’s safeApprove() pattern does not zero-out allowances (nor necessarily clear leftover allowances after use), so operations can revert.

Evidence from PoC logs:

  • “Leftover USDT-like allowance to aggregator after partial fill: 999999999999”

  • “Second call should revert due to USDT-like non-zero -> non-zero approve rule.”

  • “VULNERABILITY CONFIRMED: Approval hygiene blocks subsequent operations.”

Note: In the provided run, the second call reverted earlier during transferFrom due to the test user balance being exhausted. The key precondition (leftover non-zero allowance after partial fill) is shown; with sufficient balance the wrapper would hit safeApprove over a non-zero current allowance and revert under USDT-like rules.

Impact Details

This can cause the wrapper to be unable to operate with USDT-like tokens after a partial fill leaves a non-zero allowance, temporarily freezing affected assets and impairing UX and reliability.

Suggested Mitigation

  • Always use a reset-to-zero then set pattern for approvals:

    • If allowance(spender) != 0, call safeApprove(spender, 0) before safeApprove(spender, amount).

  • Eliminate residual allowances after use:

    • After the swap completes, safeApprove(spender, 0).

    • This reduces attack surface if a spender becomes compromised.

  • Handle partial fills to avoid leftover allowances altogether:

    • Refund unspent srcToken to the user.

    • Zero-out the allowance to the router/approver after the call.

Apply these patterns in _oneInchHelper, _okxHelper, and any other approval locations (e.g., vault approvals).

Proof of Concept

Summary: The PoC uses a USDT-like token which rejects approve() calls that move a non-zero allowance to another non-zero value. After a partial fill the wrapper leaves a non-zero allowance to the aggregator. A second call with the same token will fail unless the wrapper first resets the allowance to zero.

PoC code (place under test/ and run with provided remappings)
Execution Logs

Run: forge test -vv --match-path test/USDT_Like_Approval_DoS.t.sol

Output:

Was this helpful?