Stranded Swapper Sweep
Exploit Transactions
0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c3Victim Addresses
0xa5564a2d1190a141cac438c9fde686ac48a18a79EthereumLoss Breakdown
Similar Incidents
CauldronV4 solvency-check bypass enables uncollateralized MIM borrowing
31%USDTStaking Approval Drain
31%Flashstake LP Share Inflation
30%Orion Pool Double-Count Exploit
29%Conic crvUSD Oracle Exploit
29%VINU Reserve Drain
29%Root Cause Analysis
Stranded Swapper Sweep
1. Incident Overview TL;DR
On 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.
2. Key Background
Abracadabra's ZeroXStargateLPSwapper is a public periphery contract intended to unwind Stargate LP positions held in BentoBox. Its intended flow is:
- withdraw Stargate LP shares from BentoBox,
- redeem the LP into the underlying token through Stargate Router,
- swap the underlying token into MIM through 0x,
- deposit the MIM back into BentoBox for the designated recipient.
Three protocol details matter for this incident:
- The swapper is publicly callable. There is no caller authorization around
swap(...). - Stargate Router's
instantRedeemLocal()is permissionless but requires_amountLP > 0, so an attacker only needs a positive dust LP amount to trigger the redeem path. - The seed pre-state at block
17521638already had17,975,143,719raw 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.
3. Vulnerability Analysis & Root Cause Summary
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;
}
4. Detailed Root Cause Analysis
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:
- only
1,955,253LP had to be redeemed to reach the swap path, - the swapper redeemed only
1,957,318raw USDT in the current call, - the 0x route nevertheless spent
17,975,143,719raw USDT from the swapper, - the route transferred
17,991.956694335799894602MIM 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.
5. Adversary Flow Analysis
The adversary strategy was a single-transaction stranded-balance sweep using an EOA plus a helper contract:
- The sender EOA
0x9d4fd681aacbc49d79c6405c9aa70d1afd5accf3called helper contract0x26fe84754a1967d67b7befaa01b10d7b35bbaf0a. - The helper pulled
3,000,000raw Stargate USDT LP from the sender, approved BentoBox, and deposited2,400,000BentoBox shares to the swapper's BentoBox balance. - The helper called
ZeroXStargateLPSwapper.swap(...)with attacker-controlled 0x calldata that specified:- sell token
USDT, - buy token
MIM, - provider
0x561b94454b65614ae3db0897b74303f4acf7cc75, - recipient
0x9d4fd681aacbc49d79c6405c9aa70d1afd5accf3, - sell amount
17,975,143,719raw USDT.
- sell token
- Inside the swapper,
1,920,000BentoBox shares were withdrawn as1,955,253raw LP and redeemed into only1,957,318raw USDT. - The swapper then executed the attacker-supplied 0x route, which spent the swapper's preexisting
17,975,143,719raw USDT and transferred17,991.956694335799894602MIM directly to the sender EOA. - The swapper ended with zero MIM, zero LP, and only
1,957,318raw 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.
6. Impact & Losses
The measurable victim-side loss was the stranded USDT balance held by the public swapper:
- Token:
USDT - Raw on-chain amount:
17975143719 - Decimals:
6 - Display amount:
17,975.143719USDT
The 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.
7. References
- Seed exploit transaction:
0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c3 - Victim contract:
ZeroXStargateLPSwapperat0xa5564a2d1190a141cac438c9fde686ac48a18a79 - Verified swapper source:
https://repo.sourcify.dev/contracts/full_match/1/0xa5564a2d1190a141cac438c9fde686ac48a18a79/sources/src/swappers/ZeroXStargateLPSwapper.sol - Stargate Router source excerpt:
https://repo.sourcify.dev/contracts/full_match/1/0x8731d54e9d02c286767d56ac03e8037c07e01e98/sources/contracts/Router.sol - Stargate Pool source artifact:
/workspace/session/artifacts/collector/seed/1/0x38ea452219524bb87e18de1c24d3bb59510bd783/src/Pool.sol - Seed transaction trace:
/workspace/session/artifacts/collector/seed/1/0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c3/trace.cast.log - Seed transaction balance diff:
/workspace/session/artifacts/collector/seed/1/0x2c9f87e285026601a2c8903cf5f10e5b3655fbd0264490c41514ce073c42a9c3/balance_diff.json - Pre-state observations:
/workspace/session/artifacts/auditor/iter_0/prestate_observations.json - Profitability derivation:
/workspace/session/artifacts/auditor/iter_2/profitability_derivation.json