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 Network
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.
Was this helpful?