52980 sc critical partial fills strand source tokens in the wrapper and leave dangerous residual allowances

Submitted on Aug 14th 2025 at 14:48:17 UTC by @RevertLord for Attackathon | Plume Network

  • Report ID: #52980

  • Report Type: Smart Contract

  • Report severity: Critical

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

  • Impacts: Permanent freezing of funds

Description

Brief / Intro

A critical flaw exists in DexAggregatorWrapperWithPredicateProxy that manifests on partial fills. When a swap spends less than the full srcToken amount, the wrapper:

  • never returns the unspent remainder to the user, and

  • leaves a non-zero allowance outstanding to the router.

This traps user funds inside the wrapper and exposes dangling approvals that can be abused if the external spender (router/aggregator) is compromised.

The behavior is reproducible on the 1inch path and applies analogously to the OKX path.

Vulnerability Details

1

1inch path (non-native)

  • The wrapper pulls desc.amount of srcToken from the user and approves the router for that full amount.

  • aggregator.swap(executor, desc, data) returns (returnAmount, spentAmount), but the wrapper ignores spentAmount.

  • If spentAmount < desc.amount:

    • The difference (desc.amount − spentAmount) remains held by the wrapper.

    • The leftover allowance (same difference) stays granted to the router.

    • There is no refund of unspent srcToken to the user.

2

Native variant

  • The wrapper wraps ETH into WETH and approves the router for the WETH.

  • If the router spends only part of the WETH, the remainder stays in the wrapper with an allowance still set.

3

OKX path

  • The same approval-before-execution pattern exists.

  • If the router consumes less than the approved amount, residual tokens and allowances are left behind.

  • There is no refund mechanism for unspent balances.

Root cause: the implementation never reconciles what was actually spent versus what was fronted and approved, and it does not sanitize approvals after execution.

Impact Details

  • Severity: Critical

  • In-scope impact: Permanent freezing of user funds (the wrapper has no API to recover the stranded srcToken), plus a latent risk of direct theft via dangling approvals if the external spender becomes malicious or is exploited.

Suggested Mitigation

  • Refund unspent balances:

    • Read and use spentAmount returned by the 1inch router. Compute unspent = desc.amount − spentAmount. If unspent > 0, return the unspent srcToken to msg.sender (or return WETH/unwrapped ETH for the native path).

    • On OKX, derive “spent” via before/after balance deltas and similarly refund any unspent amount.

  • Scrub approvals:

    • After the swap completes, set allowance(spender, 0).

    • Prefer the “reset to zero then set” pattern when granting fresh allowances to support USDT-like tokens.

  • Consider using permit + exact pull semantics or streaming approvals tightly scoped to the execution.

Proof of Concept

A Foundry test (PartialFillTrappedResidual.t.sol) configures a mock 1inch router that spends only 1 wei and returns 1 wei as output. The wrapper still holds almost the entire srcToken amount, and the leftover allowance to the router equals that trapped amount.

Key logs from the PoC:

  • “Wrapper residual WETH (trapped): 999999999999999999”

  • “Residual allowance to aggregator: 999999999999999999”

This confirms both the fund-stranding and the unsafe residual approval.

PoC Code

Place the following PoC file inside the test folder in attackathon-plume-network-nucleus-boring-vault. Install all required dependencies and libraries and use the provided remappings.txt (unchanged). Once done, create the PartialFillTrappedResidual.t.sol file and paste the following code:

Execution Logs (how to run)

Run:

Expected logs (example):

Execution Logs

Ran 1 test for test/PartialFillTrappedResidual.t.sol:PartialFill_TrappedResidual_PoC [PASS] test_CRITICAL_partialFill_traps_srcToken_and_leaves_allowance() (gas: 337734) Logs: --- PoC: Partial Fill Freezes Residual srcToken in Wrapper (WETH->WETH) --- Wrapper residual WETH (trapped): 999999999999999999 Residual allowance to aggregator: 999999999999999999 VULNERABILITY CONFIRMED (CRITICAL): Partial fill froze the srcToken residual in the wrapper.

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


If you want, I can:

  • produce a patch suggestion (code diff) that implements the mitigations: reconciling spentAmount, refunding unspent tokens, and zeroing approvals safely; or

  • generate a minimal test demonstrating a fix.

Was this helpful?