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
1inch path (non-native)
The wrapper pulls
desc.amountofsrcTokenfrom the user and approves the router for that full amount.aggregator.swap(executor, desc, data)returns(returnAmount, spentAmount), but the wrapper ignoresspentAmount.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
srcTokento the user.
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
spentAmountreturned by the 1inch router. Computeunspent = desc.amount − spentAmount. Ifunspent > 0, return the unspentsrcTokentomsg.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):
If you want, I can:
produce a patch suggestion (code diff) that implements the mitigations: reconciling
spentAmount, refunding unspent tokens, and zeroing approvals safely; orgenerate a minimal test demonstrating a fix.
Was this helpful?