49863 sc critical dex aggregator erc20 token theft
Submitted on Jul 20th 2025 at 02:53:53 UTC by @Blobism for Attackathon | Plume Network
Report ID: #49863
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:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Critical: An attacker can grant themselves the ability to spend any ERC20 token that the contract holds by invoking OKX methods with a malicious teller contract and specific OKX swap parameters.
Description
Brief / Intro
An attacker can grant themselves the ability to spend any ERC20 token that the contract holds by invoking OKX methods with a malicious teller contract and specific OKX swap parameters.
Vulnerability Details
The key issue being exploited is that _okxHelper does not validate that the swap receiver is the current contract. This contrasts with _oneInchHelper, which reverts if desc.dstReceiver != address(this).
An attacker can route an OKX swap so the swapped tokens go directly to an address they control, then leverage the contract's subsequent approval logic to approve a vault they control to spend the ERC20 token the contract still holds. If the contract currently holds that token, the attacker can effectively get double the amount they swapped to.
Vulnerable excerpt from _okxHelper:
// <----------- BUG: no check that the swap receiver is the current contract
// 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
// Cast teller address to TellerWithMultiAssetSupport to call vault()
address vaultAddress = address(TellerWithMultiAssetSupport(payable(teller)).vault());
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ISSUE: teller is passed in by the user, meaning attacker controls the vault
if (vaultAddress == address(0)) {
revert("DexAggregatorWrapper: Invalid vault address for approval");
}
// Use standard approve (as requested)
supportedAsset.safeApprove(vaultAddress, supportedAssetAmount);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// attacker vault gets approved to steal ERC20 from the contract
// Return value needs to be here since it's declared in the function signature
return supportedAssetAmount;How the attacker invokes the vulnerable flow (example using depositOkxUniversal):
function depositOkxUniversal(
// ...
)
// ...
{
_checkPredicateProxy(predicateMessage);
uint256 supportedAssetAmount =
_okxHelper(supportedAsset, address(teller), fromToken, fromTokenAmount, okxCallData, nativeValueToWrap);
// Deposit assets
shares = teller.deposit(supportedAsset, supportedAssetAmount, minimumMint);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// attacker controlled teller does a fake deposit
// Get vault address
address vaultAddress = address(teller.vault());
if (vaultAddress == address(0)) {
revert("DexAggregatorWrapper: Invalid vault address");
}
// Use safeTransfer to send shares to msg.sender
ERC20(vaultAddress).safeTransfer(msg.sender, shares);
_calcSharesAndEmitEvent(
supportedAsset, CrossChainTellerBase(address(teller)), fromToken, fromTokenAmount, supportedAssetAmount
);
}Impact Details
The attack requires initial funds (flash loans possible) and an ERC20 token currently held by the contract. Leftover tokens in the contract from DEX swaps are possible (note: a distinct bug #49854 can result in tokens being left in the contract), making this vulnerability exploitable to steal those tokens.
References
See src/helper/DexAggregatorWrapperWithPredicateProxy.sol in the target repository: https://github.com/immunefi-team/attackathon-plume-network-nucleus-boring-vault/blob/main/src/helper/DexAggregatorWrapperWithPredicateProxy.sol
Proof of Concept
Was this helpful?