52178 sc critical user will lose the unspent amount when executing partial swaps via okxrouter
Submitted on Aug 8th 2025 at 14:13:41 UTC by @holydevoti0n for Attackathon | Plume Network
Report ID: #52178
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
In depositOkxUniversal and depositAndBridgeOkxUniversal from DexAggregatorWrapperWithPredicateProxy, unspent funds from a partial swap in the OKX Router are left in the wrapper contract but never deposited into the vault or returned to the user.
Vulnerability Details
The OKX Router allows partial swaps in functions like smartSwapByOrderId and smartSwapTo (both allowed to be called from DexAggregatorWrapperWithPredicateProxy).
Partial swaps occur when the sum of batchesAmount (totalBatchAmount) is less than the BaseRequest.fromTokenAmount, as enforced by a <= check in the internal _smartSwapInternal function. This design pulls only the specified totalBatchAmount from the payer, leaving any unspent input tokens in the payer's balance.
https://github.com/okxlabs/DEX-Router-EVM-V1/blob/388a4a0f70a78a525824760e18354b8ea5b8324d/contracts/8/DexRouter.sol#L312-L325
// In _smartSwapInternal
uint256 totalBatchAmount;
for (uint256 i = 0; i < batchesAmount.length; ) {
totalBatchAmount += batchesAmount[i];
unchecked {
++i;
}
}
require(
totalBatchAmount <= _baseRequest.fromTokenAmount,
"Route: number of batches should be <= fromTokenAmount"
);The router then transfers only the totalBatchAmount via _transferInternal calls in _exeForks, using IApproveProxy.claimTokens to pull from the payer (the wrapper contract).
https://github.com/okxlabs/DEX-Router-EVM-V1/blob/388a4a0f70a78a525824760e18354b8ea5b8324d/contracts/8/DexRouter.sol#L139
function _transferInternal(
address payer,
address to,
address token,
uint256 amount
) private {
if (payer == address(this)) {
SafeERC20.safeTransfer(IERC20(token), to, amount);
} else {
IApproveProxy(_APPROVE_PROXY).claimTokens(token, payer, to, amount);
}
}Problem is in the DexAggregatorWrapperWithPredicateProxy, the _okxHelper function (called by depositOkxUniversal and depositAndBridgeOkxUniversal) transfers the full fromTokenAmount from the user to the wrapper contract and approves the OKX approver for the full amount:
https://github.com/immunefi-team/attackathon-plume-network-nucleus-boring-vault/blob/0ee676b5715075c26db6706960fd49ab59b587fc/src/helper/DexAggregatorWrapperWithPredicateProxy.sol#L318-L325
function _okxHelper(
ERC20 supportedAsset,
address teller,
address fromToken,
uint256 fromTokenAmount,
bytes calldata okxCallData,
uint256 nativeValueToWrap
)
internal
returns (uint256 supportedAssetAmount)
{
// ...
ERC20 depositAsset = ERC20(fromToken);
// Use safeTransferFrom
depositAsset.safeTransferFrom(msg.sender, address(this), fromTokenAmount);
// Use standard approve for the OKX approver
depositAsset.safeApprove(okxApprover, fromTokenAmount);
// Execute the swap with the provided calldata
(bool success, bytes memory result) = address(okxRouter).call(okxCallData);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
// Decode the return value
supportedAssetAmount = abi.decode(result, (uint256));
// Approve teller's vault to spend the supported asset
// ...
return supportedAssetAmount;
}If the okxCallData specifies a partial swap, the OKX router pulls only the used amount from the wrapper, leaving the unspent input tokens in the wrapper contract. The wrapper does not account for or refund this remainder, it proceeds to deposit only the supportedAssetAmount (output from the swap) into the vault, ignoring the unspent input.
Example scenario (short):
User sends 100 USDC as
fromTokenAmount.okxCallData configures
batchesAmountsumming to 80 USDC.Wrapper transfers 100 USDC to itself.
OKX router pulls 80 USDC, performs the swap.
20 USDC remain in the wrapper, not refunded or deposited.
Impact Details
When depositing via OKX with partial swaps, unspent input tokens are left in the
DexAggregatorWrapperWithPredicateProxycontract but never returned to the user.This loss accumulates with every partial swap, potentially trapping significant value in the wrapper over time.
The wrapper keeps approving the OKX router to spend more funds than it needs.
Recommendation
Proof of Concept
Was this helpful?