52982 sc medium non standard erc20 approvals usdt like cause repeat call failures after partial fills

Submitted on Aug 14th 2025 at 14:54:07 UTC by @RevertLord for Attackathon | Plume Networkarrow-up-right

  • Report ID: #52982

  • 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

Description

Brief / Intro

A medium-severity reliability issue was identified in DexAggregatorWrapperWithPredicateProxy when interacting with tokens that require an “approve(0) before approve(new)” pattern (USDT-like semantics). After a partial fill leaves a non-zero allowance to the router, subsequent operations with the same token can fail because the wrapper attempts a non-zero→non-zero approve without first clearing the allowance.

This behavior affects any token that reverts or returns false on non-zero→non-zero approve transitions. Partial fills are common with aggregators, so this is a realistic availability issue for affected assets.

Vulnerability Details

Where it occurs:

  • oneInchHelper: safeApprove(aggregator, amount) without zeroing first.

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

  • Allowances to the vault follow the same no-zero-first pattern.

Behavior:

  • Partial fills leave a residual allowance to the router/approver.

  • On a later call with the same token, the wrapper tries to approve another non-zero amount while the current allowance is non-zero.

  • USDT-like tokens typically reject such transitions, making safeApprove revert.

This is practical rather than theoretical: partial fills are normal and USDT-like approval semantics are widespread.

Impact Details

  • Severity: Medium

  • In-scope impact: Smart contract unable to operate / temporary freezing for impacted assets. Users attempting to deposit those tokens will experience reverts until the allowance is explicitly reset.

Suggested Mitigation

  • Always adopt zero-first approvals:

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

  • Clean up after executing swaps:

    • After the router/approver has consumed what it needs, safeApprove(spender, 0) to avoid leaving stale approvals.

  • Combine with the partial-fill fix:

    • Refund unspent srcToken to avoid accumulating residual allowances in the first place.

Proof of Concept

A Foundry test (USDT_Like_Approval_DoS.t.sol) uses a USDT-like token whose approve reverts on non-zero→non-zero changes. After a partial fill, a large non-zero allowance remains to the router. A subsequent call attempts to re-approve while non-zero, which would revert under USDT-like rules, demonstrating a repeatability failure for those assets. Topping up user balances before the second attempt ensures the approve path is exercised first and the revert is attributable to approval semantics.

chevron-rightPoC test file (place under test/ and run with remappings)hashtag
chevron-rightExecution Logshashtag

Ran 1 test for test/USDT_Like_Approval_DoS.t.sol:USDTLike_Approval_DoS_PoC [PASS] test_MEDIUM_usdt_like_approval_breaks_next_call() (gas: 357478) Logs: --- PoC: USDT-like Approval Fails After Partial Fill Left Non-Zero Allowance (USDT->WETH) --- 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.

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 21.56ms (5.32ms CPU time)

Was this helpful?