Calculated from recorded token losses using historical USD prices at the incident time.
0xbac614f4d103939a9611ca35f4ec9451e1e98512d573c822fbff70fafdbbb5a00x2836B64a39d5B73d8f534c9fd6c6ABD81df2beB7BSCOn BSC block 39,684,703 (chainid 56), an unprivileged EOA 0x4CeD363484dfeBd0faB1b33C3eca0eDca44a346C sent a single transaction (0xbac614f4d103939a9611ca35f4ec9451e1e98512d573c822fbff70fafdbbb5a0) to its own orchestrator contract 0x00Db72390C1843De815ef635EE58Ac19b54AF4EF using selector 0x6053b202. The orchestrator first interacted with DysonVault 0x2836B64a39d5B73d8f534c9fd6c6ABD81df2beB7 and its strategy StrategyCommonSolidlyHybridPoolLPOvernight to harvest rewards, then called DysonVault.withdrawAll() to burn all vault shares it held and redeem the corresponding Thena USD+/USDT+ LP tokens from Pair 0x1561D9618dB2Dcfe954f5D51f4381fa99C8E5689. It subsequently unwound those LP tokens through a chain of public routers and pools into WBNB and finally executed WBNB.withdraw(52.008 BNB), paying the resulting BNB back to the EOA.
The root cause is a MEV-style, anyone-can-take (ACT) opportunity. A passive DysonVault strategy concentrates depositor TVL in a single Thena USD+/USDT+ LP position and allows any share holder to withdraw and freely trade the underlying LP in one transaction. Given the publicly accessible Thena and Overnight liquidity, an adversary can deterministically unwind its share of the vault’s LP exposure into BNB with strictly positive profit, extracting value from other LPs and swap counterparties while DysonVault and its strategy maintain correct accounting invariants.
DysonVault on BSC (vault proxy 0x2836B64a39d5B73d8f534c9fd6c6ABD81df2beB7, implementation ) is an upgradeable ERC20 vault that holds a single LP token as its underlying “want” and delegates active management to a strategy contract. The vault defines total TVL as its own want balance plus the strategy’s reported balance, and vault shares represent proportional claims on this total. In simplified form:
0x6e668080FF8Fa5F606cDC200229d054Aa5B8Fb13// DysonVault.sol (implementation at 0x6e66...fb13)
function balance() public view returns (uint256) {
return want().balanceOf(address(this)) + IStrategyDystopia(strategy).balanceOf();
}
function withdraw(uint256 _shares) public nonReentrant {
uint256 r = (balance() * _shares) / totalSupply();
_burn(msg.sender, _shares);
uint256 b = want().balanceOf(address(this));
if (b < r) {
uint256 _withdraw = r - b;
strategy.withdraw(_withdraw);
uint256 _after = want().balanceOf(address(this));
uint256 _diff = _after - b;
if (_diff < _withdraw) {
r = b + _diff;
}
}
want().safeTransfer(msg.sender, r);
}
The attached strategy is StrategyCommonSolidlyHybridPoolLPOvernight (implementation 0xaFF18b43Dfb44d9b56C2B88e8569b3B0880C2a56), which manages a Solidly-style hybrid pool on Thena for Overnight’s USD+ and USDT+. It stakes LP tokens in GaugeV2 0x3877c2C3D75aE80f2Ed8E9d4d68e3C1BFc77e5A6, harvests emissions into an output token, swaps the output into underlying stablecoins, mints USD+/USDT+ via Overnight exchanges, and adds liquidity to Thena Pair 0x1561D9618dB2Dcfe954f5D51f4381fa99C8E5689. A core part of the strategy is:
// StrategyCommonSolidlyHybridPoolLPOvernight.sol (impl 0xaFF1...2a56), addLiquidity()
function addLiquidity() internal override {
uint256 outputBal = IERC20Upgradeable(output).balanceOf(address(this));
uint256 lp0Amt = outputBal / 2;
uint256 lp1Amt = outputBal - lp0Amt;
// swap output -> lpToken0 / lpToken1 via public router
// mint USD+ / USDT+ via Overnight
// add liquidity to Thena pair and receive LP (want)
ISolidlyRouter(dystRouter2).addLiquidity(
usdPlus,
usdtPlus,
stable,
lp0Bal,
lp1Bal,
1,
1,
address(this),
block.timestamp
);
}
This design means DysonVault depositors are collectively exposed to a single LP position in the Thena USD+/USDT+ pool, whose value is determined by AMM pricing, Overnight rebasing, and external market conditions. Any address holding vault shares can invoke withdraw() or withdrawAll() to pull its proportional share of the underlying LP out of the vault.
The adversary uses a custom orchestrator contract 0x00Db72390C1843De815ef635EE58Ac19b54AF4EF, deployed by EOA 0x4CeD...346C at tx 0xabc74eea51706837d3790427e119cee1ca490ad81c3ddcd03137a2d7ca702af6. The orchestrator exposes a function with selector 0x6053b202 and is called exclusively by the same EOA in the collected tx history. Its purpose is to bundle a strategy harvest, vault withdrawal, LP redemption via Thena Pair, and multi-hop swaps through public routers, culminating in a WBNB withdrawal to BNB for the EOA.
At the contract level, DysonVault and its strategy preserve a standard vault accounting invariant: at all times, the total underlying LP balance equals the vault’s idle LP plus the strategy’s LP balance, and each user’s claim on the underlying is proportional to their vault shares. There is no evidence of reentrancy, access-control failure, or mis-accounting in either DysonVault or StrategyCommonSolidlyHybridPoolLPOvernight.
The vulnerability arises at the system level. The vault strategy concentrates TVL in a single Thena USD+/USDT+ LP position and exposes that LP to any vault share holder via withdrawAll(). Given existing Thena/Overnight/Algebra liquidity and pricing, an adversary can construct a cyclic transaction that (a) begins and ends with only BNB in an unprivileged EOA and its orchestrator, (b) uses withdrawn LP as the only link to DysonVault, and (c) produces strictly positive net BNB profit by trading against public pools, with losses absorbed by other LPs and swap counterparties.
In the observed incident, the adversary executes exactly such a cycle: harvest → withdraw all DysonVault shares → redeem the resulting LP in Thena Pair → route the stablecoins through public routers into WBNB → withdraw WBNB into BNB. The vault and strategy invariants hold throughout, but the combined economic state of DysonVault depositors and Thena/Overnight LPs shifts to the adversary’s advantage, making this a MEV-style ACT opportunity rooted in protocol design rather than a localized smart contract bug.
DysonVault’s implementation enforces a standard vault invariant:
balance_t = want(vault)_t + strategy.balanceOf_t.shares_t, the claim on underlying is user_underlying_t = balance_t * shares_t / totalSupply_t.The vault’s deposit(), earn(), withdraw(), and withdrawAll() functions, together with the strategy’s deposit() and withdraw(), preserve this invariant. In particular, withdrawals burn shares first, then request underlying LP from the strategy, and will only reduce the redeemed amount if the strategy cannot return the requested LP. There are no code paths that mint extra shares, transfer want to arbitrary addresses, or mis-report balanceOf(). Our inspection of DysonVault.sol and StrategyCommonSolidlyHybridPoolLPOvernight.sol confirms that, for the seed transaction, the vault and strategy logic behave as designed.
At the system level (DysonVault depositors + Thena/Overnight LPs + the adversary cluster), a natural economic invariant is:
Equivalently, the aggregate BNB-valued portfolio of (adversary cluster + DysonVault depositors + Thena/Overnight LPs) should be conserved up to fees across any single-transaction unwind of a vault LP position.
The breakpoint occurs when the adversary uses its DysonVault shares to access the underlying LP and then trades against public liquidity in a way that violates this system-level invariant, while leaving the vault’s local invariant intact.
The seed transaction is:
0xbac614f4d103939a9611ca35f4ec9451e1e98512d573c822fbff70fafdbbb5a00x4CeD363484dfeBd0faB1b33C3eca0eDca44a346C0x00Db72390C1843De815ef635EE58Ac19b54AF4EF0x6053b202The collected trace.cast.log for this transaction shows the following key steps:
SM Address: 0x6e668080ff8fa5f606cdc200229d054aa5b8fb13, caller:0x00db72390c1843de815ef635ee58ac19b54af4ef,target:0x2836b64a39d5b73d8f534c9fd6c6abd81df2beb7
├─ 0x2836B64a39d5B73d8f534c9fd6c6ABD81df2beB7::withdrawAll()
│ ├─ 0x6e668080FF8Fa5F606cDC200229d054Aa5B8Fb13::withdrawAll() [delegatecall]
│ │ ├─ StrategyCommonSolidlyHybridPoolLPOvernight::withdraw(17139886497691096)
│ │ ├─ GaugeV2::withdraw(17139886497691096)
...
│ │ ├─ emit Burn(sender: 0x00Db72390C1843De815ef635EE58Ac19b54AF4EF,
│ │ │ amount0: 15571186191039571180026,
│ │ │ amount1: 18734917395,
│ │ │ to: 0x00Db72390C1843De815ef635EE58Ac19b54AF4EF)
...
├─ WBNB::withdraw(52008000000000000000)
│ ├─ emit Withdrawal(src: 0x00Db72390C1843De815ef635EE58Ac19b54AF4EF,
│ │ wad: 52008000000000000000)
From this trace and the associated balance_diff.json, the concrete mechanism is:
Strategy harvest and preparation
The orchestrator first triggers a harvest path on StrategyCommonSolidlyHybridPoolLPOvernight, which realizes accrued rewards, routes them through Overnight exchanges, and adds liquidity to the Thena USD+/USDT+ pair. This step does not violate any local invariants but may change the composition and size of the LP position.
Vault withdrawal of adversary’s shares
The orchestrator calls DysonVault.withdrawAll() on vault 0x2836...beb7 from its own address 0x00Db...4EF. The vault, via its implementation, burns all shares held by the orchestrator and requests the corresponding amount of LP tokens (want) from the strategy. The strategy withdraws 17139886497691096 LP tokens from GaugeV2 and returns them to the vault, which then transfers those LP tokens to the orchestrator as the withdraw recipient. At this point:
balance = want(vault) + strategy.balanceOf() still holds.Thena Pair LP burn and reserve shift
Holding the LP tokens, the orchestrator interacts with Thena Pair 0x1561...5689:
Pair::burn(0x00Db...4EF) is executed, emitting a Burn event where:
amount0 = 15571186191039571180026 (UsdPlusToken units),amount1 = 18734917395 (paired-asset units),to = 0x00Db...4EF.balance_diff.json.
This step realizes the adversary’s share of the underlying stablecoin position and shifts the Thena pool’s reserves, with passive LPs bearing the opposite side of the trade.Multi-hop swaps into WBNB
The orchestrator then routes the received stable balances through a fixed chain of public swap contracts:
0x51Bd5e6d3da9064D59BcaA5A76776560aB42cEb8,0x327Dd3208f0bCF590A66110aCB6e5e6941A4EfA0,0x1b9a1120a17617D8eC4dC80B921A9A1C50Caef7d,0xd99c7F6C65857AC913a8f880A4cb84032AB2FC5b,0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE.The trace shows multiple Swap events on these contracts, with large stablecoin amounts in and WBNB amounts out, leaving intermediate LPs and counterparties with less favorable token compositions.
WBNB withdrawal to BNB
Finally, the orchestrator calls WBNB.withdraw(52008000000000000000) on WBNB 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c. The WBNB contract reduces its internal balance by 52.008 BNB and sends the same amount of native BNB to the orchestrator, as shown in the Withdrawal event and in the native balance deltas.
From balance_diff.json, the net result in BNB is:
{
"native_balance_deltas": [
{
"address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
"delta_wei": "-52008000000000000000"
},
{
"address": "0x4ced363484dfebd0fab1b33c3eca0edca44a346c",
"before_wei": "1317724903000000000",
"after_wei": "53304321053000000000",
"delta_wei": "51986596150000000000"
}
]
}
Gas usage is 2,680,770 at 5 gwei (0.01340385 BNB). Thus:
This profit is obtained in a single transaction, without any new DysonVault shares or persistent LP exposure for the adversary at the end, and with all contract-level invariants satisfied. The “missing” BNB comes from the WBNB contract and, economically, from liquidity providers and counterparties in the stablecoin and WBNB pools that were traded against along the path.
The adversary-related cluster consists of:
0x4CeD363484dfeBd0faB1b33C3eca0eDca44a346C (sender, profit recipient, and deployer of orchestrator).0x00Db72390C1843De815ef635EE58Ac19b54AF4EF (callee of 0x6053b202 that bundles the exploit path).This cluster is minimal: no additional helper contracts are required, and all other contracts involved (DysonVault, strategy, Thena Pair and Gauge, Overnight exchanges, routers, WBNB) are public and permissionless.
The adversary’s execution flow in the seed transaction is:
Preparation (off-chain / prior state)
StrategyCommonSolidlyHybridPoolLPOvernight, and the vault TVL is concentrated in the Thena USD+/USDT+ LP position.Step 1 – Orchestrator call
0xbac6...bb5a0 calling 0x00Db...4EF with selector 0x6053b202. No access control or whitelist gates this call; it is a standard public transaction with 5 gwei gas price and sufficient gas limit.Step 2 – Strategy harvest
StrategyCommonSolidlyHybridPoolLPOvernight, which claims accumulated rewards, swaps them into desired assets, mints Overnight USD+/USDT+, and adds liquidity back into the Thena pair. This increases or reshapes the vault’s LP exposure but does not directly realize profit for the adversary.Step 3 – Vault withdrawAll
DysonVault.withdrawAll() from its own address, causing the vault to:
balance() based on its vault shares.17139886497691096 units) to the orchestrator.Step 4 – LP redemption via Thena Pair::burn
0x1561...5689:
Pair::burn(0x00Db...4EF) is called, which burns the LP and sends out the underlying USD+/USDT+ balances to the orchestrator, while updating pool reserves.Step 5 – Multi-hop swaps to WBNB
amount0In / amount1Out values for these swaps, indicating significant stable-to-WBNB conversion.Step 6 – WBNB withdrawal to BNB
WBNB.withdraw(52.008 BNB), which reduces WBNB’s internal balances and transfers 52.008 BNB to the orchestrator. After accounting for gas, the EOA ends the transaction with 51.9731923 BNB more than it started with, and with no remaining DysonVault shares or LP tokens from this cycle.Because all contracts involved are public and unprivileged, and the orchestrator is merely a convenience wrapper, any actor with DysonVault shares in the same pre-state could deploy an equivalent orchestrator and replay this strategy. This satisfies the ACT adversary model: the opportunity is not tied to special permissions, only to access to shares and public liquidity.
From balance_diff.json and the seed trace:
0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c) loses 52.008 BNB via withdraw, consistent with the WBNB contract semantics.The economic loss is distributed across:
There is no on-chain evidence of insolvency in DysonVault, but the incident demonstrates that a passive LP vault design, combined with deep public AMM liquidity, can expose users to deterministic MEV extraction on their pooled position.
0xbac614f4d103939a9611ca35f4ec9451e1e98512d573c822fbff70fafdbbb5a0 (EOA → orchestrator).0x4CeD363484dfeBd0faB1b33C3eca0eDca44a346C0x00Db72390C1843De815ef635EE58Ac19b54AF4EF (deployed in tx 0xabc74eea51706837d3790427e119cee1ca490ad81c3ddcd03137a2d7ca702af6).0x2836B64a39d5B73d8f534c9fd6c6ABD81df2beB7 (implementation at 0x6e668080FF8Fa5F606cDC200229d054Aa5B8Fb13).0xaFF18b43Dfb44d9b56C2B88e8569b3B0880C2a56 (proxy 0x2b9BDa587ee04fe51C5431709afbafB295F94bB4).0x1561D9618dB2Dcfe954f5D51f4381fa99C8E5689.0x3877c2C3D75aE80f2Ed8E9d4d68e3C1BFc77e5A6.0xe80772Eaf6e2E18B651F160Bc9158b2A5caFCA65 (implementation 0x6002054688d62275d80cc615f0f509d9b2ff520d).0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c.0xbac6...bb5a0.trace.cast.log showing DysonVault.withdrawAll, strategy withdraw, Pair::burn, swap, and WBNB::withdraw calls and events.balance_diff.json for tx 0xbac6...bb5a0, showing native and ERC20 balance changes used in the profit calculation.