52923 sc critical partial fill traps source token residual inside the wrapper and leaves unsafe residual allowance
Submitted on Aug 14th 2025 at 10:43:30 UTC by @LoopGhost007 for Attackathon | Plume Network
Report ID: #52923
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
I have identified a critical vulnerability in DexAggregatorWrapperWithPredicateProxy where partial fills during swaps (via 1inch path) can permanently trap the remaining source tokens (srcToken) inside the wrapper contract and leave a non-zero residual allowance to the router. This occurs because the wrapper ignores the router-reported spent amount and never refunds the unspent portion to the user, nor does it sanitize the allowance afterwards.
In the reproduced PoC, a swap that spends only 1 wei of WETH out of 1 ETH causes 0.999999999999999999 WETH to remain stuck in the wrapper, with the same amount left as residual allowance to the router.
Vulnerability Details
It all happens in the following functions of DexAggregatorWrapperWithPredicateProxy.sol:
_oneInchHelper(non-native path)depositOneInch(calls_oneInchHelper)Similarly applicable in
_okxHelper(OKX path) and its public entrypoints
Problematic flow (1inch, non-native path)
The vulnerable sequential flow:
The wrapper pulls
desc.amountofsrcTokenfrom the user andsafeApprove()’s the aggregator for that full amount.The router executes a swap which may partially fill, spending only a portion of
desc.amountand returning(returnAmount, spentAmount).The wrapper ignores
spentAmountand never refunds(desc.amount - spentAmount)to the user.The wrapper also does not clear the residual allowance
(desc.amount - spentAmount)to the router.The unspent
srcTokenremains stuck in the wrapper indefinitely, and the leftover allowance presents an extra risk surface.
Partial fill is an operational reality for modern DEX aggregators (including 1inch v6), which is why their interface returns spentAmount. The current implementation simply doesn’t use it.
Evidence from the PoC logs:
Wrapper residual WETH (trapped): 999999999999999999
Residual allowance to aggregator: 999999999999999999
This demonstrates both trapped funds and unsafe leftover allowance.
Impact Details
Critical-severity permanent freezing of user funds. Additional risks:
A malicious/compromised router or approved spender could pull the trapped tokens later using the leftover allowance.
Even without an active pull, funds are effectively frozen since the wrapper exposes no user-facing method to recover the residual
srcToken.
Suggested Mitigation
Refund unspent
srcTokenon partial fills:Read
spentAmountfrom the router’s return value.Compute
unspent = desc.amount - spentAmount.If
unspent > 0:Non-native path: transfer unspent
srcTokenback tomsg.sender.Native path (WETH): either transfer WETH back or unwrap to ETH and refund.
Sanitize allowances:
After executing the swap, set allowance(aggregator, 0) to avoid dangling approvals.
Prefer “reset-to-zero then set” pattern for tokens with non-standard approval semantics.
Apply same logic to the OKX path in
_okxHelper:Detect actual spent via balance deltas around the router call.
Refund any unspent amount to the user and zero-out the allowance to
okxApprover.
Proof of Concept
The PoC is a Foundry test that configures a mock 1inch router to spend only 1 wei of srcToken and return 1 wei of dstToken. The wrapper pulls 1 ETH of WETH from the user, approves the router for the full amount, and then ends up with ~1 ETH trapped plus the same amount left as residual allowance.
This confirms the issue in a realistic setup using the in-scope wrapper and the project’s teller/vault stack.
Execution Logs
Run forge test -vv --match-path test/PartialFillTrappedResidual.t.sol. Example output from PoC:
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:
Suggest small, concrete code patches to fix the exact functions (
_oneInchHelper,_okxHelper) that readspentAmount, refund unspent tokens, and zero allowances; orProduce a unit test that validates the fix (ensures no trapped balance and allowance is zero after partial fill).
Was this helpful?