Calculated from recorded token losses using historical USD prices at the incident time.
0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c30xa5564a2d1190a141cac438c9fde686ac48a18a79EthereumOn Ethereum mainnet transaction 0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c3 at block 17521639, an unprivileged adversary drained 17,975.143719 USDT that had been left stranded on Abracadabra's public ZeroXStargateLPSwapper at 0xa5564a2d1190a141cac438c9fde686ac48a18a79. The attacker only needed dust Stargate USDT LP to satisfy the swapper's redeem path, then supplied arbitrary 0x calldata that sold the swapper's preexisting USDT and sent 17,991.956694335799894602 MIM directly to the attacker's EOA.
The root cause is an attack-class logic failure in ZeroXStargateLPSwapper.swap(address,address,address,uint256,uint256,bytes): the contract grants the 0x exchange proxy a standing unlimited allowance over USDT, executes arbitrary caller-supplied swapData, and only deposits the swapper's residual mim.balanceOf(address(this)) into BentoBox after the external call. That combination lets any public caller redirect swap output away from the swapper and sweep any preexisting underlying-token balance held by the contract.
Abracadabra's ZeroXStargateLPSwapper is a public periphery contract intended to unwind Stargate LP positions held in BentoBox. Its intended flow is:
Three protocol details matter for this incident:
swap(...).instantRedeemLocal() is permissionless but requires _amountLP > 0, so an attacker only needs a positive dust LP amount to trigger the redeem path.17521638 already had 17,975,143,719 raw USDT on the swapper, while the swapper held no Stargate LP and no DegenBox share balance of that LP.That pre-state is what turned a public deleverage helper into an ACT opportunity: once stranded USDT was left on the contract, any caller who could reach the redeem path and control the 0x route could sell that USDT.
This is an ATTACK root cause, not a benign MEV unwind. The vulnerable contract itself exposes the unsafe behavior. In its constructor, ZeroXStargateLPSwapper gives the 0x exchange proxy an unlimited approval over the Stargate pool's underlying token and gives BentoBox an unlimited approval over MIM. In swap(...), it withdraws LP from BentoBox, redeems all LP held by the swapper into the underlying token, and then performs a raw zeroXExchangeProxy.call(swapData) with attacker-controlled calldata. The function never checks that the 0x route only spends the newly redeemed amount, never checks that the buy-token recipient is the swapper, and never reconciles the bought amount against an expected output. After the external call returns, the function deposits only mim.balanceOf(address(this)) into BentoBox, so a route that sends all MIM directly to the attacker still succeeds because the subsequent BentoBox deposit can be zero. The violated invariant is straightforward: a public deleverage swapper must only convert the LP redeemed for the current call, and the purchased MIM must stay on the swapper so BentoBox can credit the intended recipient.
Verified swapper source:
constructor(...) {
...
underlyingToken.safeApprove(_zeroXExchangeProxy, type(uint256).max);
mim.approve(address(_bentoBox), type(uint256).max);
}
function swap(
address,
address,
address recipient,
uint256 shareToMin,
uint256 shareFrom,
bytes calldata swapData
) public override returns (uint256 extraShare, uint256 shareReturned) {
bentoBox.withdraw(IERC20(address(pool)), address(this), address(this), 0, shareFrom);
uint256 amount = IERC20(address(pool)).balanceOf(address(this));
stargateRouter.instantRedeemLocal(poolId, amount, address(this));
require(IERC20(address(pool)).balanceOf(address(this)) == 0, "Cannot fully redeem");
(bool success, ) = zeroXExchangeProxy.call(swapData);
if (!success) revert ErrSwapFailed();
(, shareReturned) = bentoBox.deposit(mim, address(this), recipient, mim.balanceOf(address(this)), 0);
extraShare = shareReturned - shareToMin;
}
The ACT opportunity is defined on Ethereum mainnet after block 17521638. At that point the swapper already held 17,975,143,719 raw USDT, but held zero Stargate LP and zero BentoBox share balance for that LP. The attacker did not need any privileged state transition; they only needed to supply a positive LP amount and a custom 0x route.
Stargate's public redeem path is the first enabler. Router code requires only a positive LP amount:
function instantRedeemLocal(
uint16 _srcPoolId,
uint256 _amountLP,
address _to
) external override nonReentrant returns (uint256 amountSD) {
require(_amountLP > 0, "Stargate: not enough lp to redeem");
Pool pool = _getPool(_srcPoolId);
amountSD = pool.instantRedeemLocal(msg.sender, _amountLP, _to);
}
Once the attacker deposits even a dust share of Stargate LP into BentoBox for the swapper, ZeroXStargateLPSwapper.swap() can withdraw that LP and redeem it into a small amount of USDT. The critical failure is that the swapper then executes arbitrary 0x calldata against its entire approved USDT balance, not just the USDT redeemed in the current call.
The seed transaction trace shows the full breakpoint:
0xa5564a2d1190a141CAC438c9fde686aC48a18A79::swap(..., shareFrom = 1920000, ...)
DegenBox::withdraw(... amount: 1955253, share: 1920000)
0x8731d54E9D02c286767d56ac03e8037C07e01e98::instantRedeemLocal(2, 1955253, swapper)
Pool::instantRedeemLocal(swapper, 1955253, swapper)
emit Transfer(from: Pool, to: swapper, value: 1957318)
0xDef1C0ded9bec7F1a1670819833240f027b25EfF::sellToLiquidityProvider(... sellAmount = 17975143719 ...)
0xdAC17F...::transferFrom(swapper, 0x561B94454b..., 17975143719)
MagicInternetMoneyV1::transfer(attackerEOA, 17991956694335799894602)
DegenBox::deposit(MIM, swapper, helper, 0, 0)
That trace proves four things at once:
1,955,253 LP had to be redeemed to reach the swap path,1,957,318 raw USDT in the current call,17,975,143,719 raw USDT from the swapper,17,991.956694335799894602 MIM directly to the attacker's EOA, leaving the swapper with zero MIM and allowing a zero-amount BentoBox deposit.The pre-state and post-state economics are also deterministic. The adversary cluster's input was the 3,000,000 raw Stargate LP moved from the sender to the helper, valued via the pool's pre-state accounting at 3.003169 USD-equivalent. After execution, the helper retained 555,933 raw LP worth 0.556520 USD-equivalent, the sender EOA received 17,991.956694335799894602 MIM, and gas cost was exactly 11,600,916,893,217,212 wei, derived from 591452 * 19614299881 and matching the sender's native balance delta. Using Chainlink ETH/USD answer 171957000000 at block 17521639, fees were 19.94858866206952123884 USD, giving a post-state adversary-cluster value of 17972.56462567373037336316 USD and a net profit of 17969.56145667373037336316 USD.
The adversary strategy was a single-transaction stranded-balance sweep using an EOA plus a helper contract:
0x9d4fd681aacbc49d79c6405c9aa70d1afd5accf3 called helper contract 0x26fe84754a1967d67b7befaa01b10d7b35bbaf0a.3,000,000 raw Stargate USDT LP from the sender, approved BentoBox, and deposited 2,400,000 BentoBox shares to the swapper's BentoBox balance.ZeroXStargateLPSwapper.swap(...) with attacker-controlled 0x calldata that specified:
USDT,MIM,0x561b94454b65614ae3db0897b74303f4acf7cc75,0x9d4fd681aacbc49d79c6405c9aa70d1afd5accf3,17,975,143,719 raw USDT.1,920,000 BentoBox shares were withdrawn as 1,955,253 raw LP and redeemed into only 1,957,318 raw USDT.17,975,143,719 raw USDT and transferred 17,991.956694335799894602 MIM directly to the sender EOA.1,957,318 raw USDT dust; BentoBox then accepted a zero-amount MIM deposit without reverting.This satisfies the ACT definition cleanly: every step uses public contracts, public state, public routing, and fresh adversary-controlled identities. No privileged signer, governance action, or attacker-owned historical contract was required to realize the opportunity.
The measurable victim-side loss was the stranded USDT balance held by the public swapper:
USDT17975143719617,975.143719 USDTThe attacker realized 17,991.956694335799894602 MIM and a net profit of 17969.56145667373037336316 USD after gas under the valuation method documented above. Because the vulnerable path is permissionless and the pre-state required only stranded USDT plus a positive LP dust amount, any unprivileged actor could have extracted the same value from that pre-state.
0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c3ZeroXStargateLPSwapper at 0xa5564a2d1190a141cac438c9fde686ac48a18a79https://repo.sourcify.dev/contracts/full_match/1/0xa5564a2d1190a141cac438c9fde686ac48a18a79/sources/src/swappers/ZeroXStargateLPSwapper.solhttps://repo.sourcify.dev/contracts/full_match/1/0x8731d54e9d02c286767d56ac03e8037c07e01e98/sources/contracts/Router.sol/workspace/session/artifacts/collector/seed/1/0x38ea452219524bb87e18de1c24d3bb59510bd783/src/Pool.sol/workspace/session/artifacts/collector/seed/1/0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c3/trace.cast.log/workspace/session/artifacts/collector/seed/1/0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c3/balance_diff.json/workspace/session/artifacts/auditor/iter_0/prestate_observations.json/workspace/session/artifacts/auditor/iter_2/profitability_derivation.json