Calculated from recorded token losses using historical USD prices at the incident time.
0x9983ca8eaee9ee69629f74537eaf031272af75f1e5a7725911d8b06df17c67ca0xd188492217f09d18f2b0ece3f8948015981e961aBSC0x9ad45d46e2a2ca19bbb5d5a50df319225ad60e0dBSCOn BNB Smart Chain, transaction 0x9983ca8eaee9ee69629f74537eaf031272af75f1e5a7725911d8b06df17c67ca let an unprivileged attacker drain StableCoinVault 0xd188492217f09d18f2b0ece3f8948015981e961a by calling its public _swap helper with attacker-chosen 1inch calldata. The call approved the configured 1inch router for the vault's full ApolloX-ALP share balance, then executed unoswapTo so the router pulled all 8693190985141166818414 ApolloX-ALP shares from the victim into an attacker-controlled helper contract. That helper then redeemed the stolen shares through ApolloXBscVault and forwarded 10610921469569473910990 raw USDT units to attacker EOA 0xff61ba33ed51322bb716eab4137adf985644b94d.
The root cause is an authorization-free external-call helper in BasePortfolioV2._swap. It grants approval over the portfolio's entire token balance and blindly forwards arbitrary calldata to the configured aggregator, breaking the invariant that strategy-vault share tokens held by the portfolio can move only through authenticated portfolio flows.
StableCoinVault is a portfolio wrapper that holds strategy-vault share tokens directly in the portfolio contract. In this incident the relevant share token is ApolloX-ALP at 0x9ad45d46e2a2ca19bbb5d5a50df319225ad60e0d, which is also the ApolloXBscVault share token.
ApolloXBscVault is redeemable by the current share holder. Its redemption path unstakes the underlying ApolloX position, burns ALP, and transfers the selected output token back to the caller. That matters because once the attacker pulls portfolio-held ApolloX-ALP shares out of StableCoinVault, no additional privilege is needed to monetize them.
The victim portfolio was configured to use the 1inch router 0x1111111254EEB25477B68fb85Ed929f73A960582. The exploit uses that trusted router as the spend path after forcing an approval from the victim contract.
This is an ATTACK root cause, not a pure MEV opportunity. The decisive issue is that BasePortfolioV2._swap is public, has no authorization checks, and accepts arbitrary aggregatorData. Because it force-approves oneInchAggregatorAddress for the portfolio's full balance of the caller-selected token, any external caller can convert _swap into an arbitrary asset-spend primitive over portfolio-held tokens.
The critical invariant is straightforward: strategy-vault share tokens held by the portfolio must only leave the portfolio through authenticated deposit, redeem, or reward-handling flows that preserve portfolio accounting. _swap violates that invariant by approving and spending those shares without any caller validation, token allowlist, or calldata constraints.
The verified victim code shows the breakpoint directly:
function _swap(
IERC20 tokenForSwap,
bytes memory aggregatorData
) public returns (uint256) {
SafeERC20.forceApprove(
tokenForSwap,
oneInchAggregatorAddress,
tokenForSwap.balanceOf(address(this))
);
(bool succ, bytes memory data) = address(oneInchAggregatorAddress).call(
aggregatorData
);
require(succ, "Aggregator failed to swap...");
return abi.decode(data, (uint256));
}
Once the attacker selects tokenForSwap = ApolloX-ALP and provides malicious-but-valid 1inch calldata, the victim portfolio becomes the approval source for its own theft.
The pre-state at block 36727073 was exploitable because StableCoinVault held 8693190985141166818414 ApolloX-ALP shares and already trusted the 1inch router. The attacker only needed a helper contract that satisfied the callback shape expected by unoswapTo.
The on-chain trace captures the exploit path exactly:
0xD188492217F09D18f2B0ecE3F8948015981e961a::_swap(ApolloX-ALP, attackerData)
0x1111111254EEB25477B68fb85Ed929f73A960582::unoswapTo(helper, ApolloX-ALP, 8693190985141166818414, 0, ...)
0x9Ad45D46e2A2ca19BBB5D5a50Df319225aD60e0d::transferFrom(victim, helper, 8693190985141166818414)
helper::getReserves()
helper::swap(...)
That transferFrom is the custody break. The victim's balance diff confirms the full share drain:
{
"token": "0x9ad45d46e2a2ca19bbb5d5a50df319225ad60e0d",
"holder": "0xd188492217f09d18f2b0ece3f8948015981e961a",
"before": "8693190985141166818414",
"after": "0",
"delta": "-8693190985141166818414"
}
After receiving the stolen shares, the helper redeemed them permissionlessly through ApolloXBscVault. The relevant protocol code is:
function _redeemFrom3rdPartyProtocol(
uint256 shares,
RedeemData calldata redeemData
) internal override returns (uint256, address, address, bytes calldata) {
apolloX.unStake(shares);
SafeERC20.forceApprove(ALP, address(apolloX), shares);
apolloX.burnAlp(redeemData.apolloXRedeemData.alpTokenOut, shares, redeemData.apolloXRedeemData.minOut, address(this));
...
SafeERC20.safeTransfer(IERC20(redeemData.apolloXRedeemData.alpTokenOut), msg.sender, redeemAmount);
}
The trace then shows unStake(8693190985141166818414) and burnAlp(...), which is exactly how the stolen shares become liquid USDT. No private key, whitelist, or privileged contract path is involved at any stage.
The adversary cluster is:
0xff61ba33ed51322bb716eab4137adf985644b94d, which sent the exploit transaction and received final profit.0x0edf13f6bd033f0f267d46c6e9dff9c7190e0fa0, which impersonated a 1inch pool callback target, received the stolen shares, redeemed them, and transferred USDT to the EOA.The exploit flow is a single transaction:
_swap with tokenForSwap = ApolloX-ALP and crafted unoswapTo calldata._swap force-approves the 1inch router for the victim's full ApolloX-ALP balance and forwards the calldata.transferFrom(victim, helper, fullBalance) and then calls the helper's getReserves and swap functions, which return successfully and keep the shares under attacker control.The profit realization is visible in the balance diff:
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0xff61ba33ed51322bb716eab4137adf985644b94d",
"before": "0",
"after": "10610921469569473910990",
"delta": "10610921469569473910990"
}
StableCoinVault lost its entire ApolloX-ALP position in the exploited portfolio instance. The attacker realized 10610921469569473910990 raw USDT units, with decimal = 18, as the measurable proceeds of the theft.
The broader impact is systemic to any similarly configured portfolio instance: if a portfolio holds transferable strategy shares and exposes the same public _swap primitive against a trusted router, any unprivileged caller can attempt the same approval-and-drain pattern.
0x9983ca8eaee9ee69629f74537eaf031272af75f1e5a7725911d8b06df17c67ca0xd188492217f09d18f2b0ece3f8948015981e961a0x1eabb7b90fac3b4143c551a02b41e741f7457efa0x0edf13f6bd033f0f267d46c6e9dff9c7190e0fa00xff61ba33ed51322bb716eab4137adf985644b94d0x9ad45d46e2a2ca19bbb5d5a50df319225ad60e0d0x55d398326f99059ff775485246999027b31979550x1111111254EEB25477B68fb85Ed929f73A960582metadata.json, trace.cast.log, and balance_diff.json for the exploit tx under /workspace/session/artifacts/collector/seed/56/0x9983ca8eaee9ee69629f74537eaf031272af75f1e5a7725911d8b06df17c67ca/