For the complete documentation index, see llms.txt. This page is also available as Markdown.

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?