StakeStone StoneVault Over-Withdrawal
Exploit Transactions
0xbf5b2d22fa88965ddfc6e6d685fc7cfc683340c49e126386759ed9e4027b1415Victim Addresses
0xa62f9c5af106feee069f38de51098d9d81b90572Ethereum0x396abf9ff46e21694f4ef01ca77c6d7893a017b2EthereumLoss Breakdown
Similar Incidents
Hegic WBTC Pool Repeated Tranche Withdrawal Exploit
33%ENF Redeem Decimal Mis-Scaling
31%Conic crvUSD Oracle Exploit
30%Metalend Empty-Market Donation Exploit
29%Euler DAI Reserve Donation
29%Raft cbETH Share Inflation
29%Root Cause Analysis
StakeStone StoneVault Over-Withdrawal
1. Incident Overview TL;DR
On Ethereum mainnet transaction 0xbf5b2d22fa88965ddfc6e6d685fc7cfc683340c49e126386759ed9e4027b1415 in block 18523441, an unprivileged adversary used an Aave flash loan to mint STONE, push StakeStone's idle ETH into strategies with rollToNextRound(), and then immediately burn the freshly minted STONE through instantWithdraw(). The key failure is that StakeStone's StoneVault computed a share-based withdrawal entitlement, but then paid the raw value returned by StrategyController.forceWithdraw() even when that downstream return exceeded the requested shortfall. The trace shows the vault asked strategies for 8579775372585181673317 wei and accepted 8617073005336909691789 wei back, so the vault overpaid by 37297632751728018472 wei, or 37.297632751728018472 ETH.
The attacker cluster consisted of sender EOA 0x9abe851bcc4fd1986c3d1ef8978fad86a26a0c57 and flash-loan receiver contract 0x9c52c485edd3d22847a1614b8988fbf520b33047. The receiver contract finished the transaction with 3 ETH, while the sender EOA paid 0.119363317574586483 ETH in gas.
2. Key Background
StakeStone's StoneVault is a round-based ETH vault that mints STONE shares. User share pricing depends on the vault's idle ETH in AssetsVault plus the strategy value reported by StrategyController.getAllStrategiesValue().
Two public entrypoints matter for the incident:
rollToNextRound()rebalances idle ETH into the configured strategy set and advances the round.instantWithdraw(uint256 amount, uint256 shares)burns shares, computes the caller's ETH entitlement, and sources any shortfall from strategies throughStrategyController.forceWithdraw(uint256).
That design only remains safe if the amount paid to the caller is capped by the caller's entitlement. In StakeStone's implementation, that cap is missing across the StoneVault -> StrategyController -> Strategy boundary.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class ACT issue caused by broken cross-module accounting. StoneVault.instantWithdraw() correctly computes a share-based ETH entitlement from burned STONE, but it does not enforce that the later strategy-sourced payout stays within that entitlement. Instead, once idle ETH is exhausted, the vault asks StrategyController.forceWithdraw() for the remaining shortfall and then blindly adds the controller's returned value to actualWithdrawn.
StrategyController.forceWithdraw() is itself an unchecked aggregation layer. It walks every configured strategy, calls each strategy's instantWithdraw(withAmount), sums the returned amounts, and repays all ETH back into AssetsVault. Nothing in that function verifies that the aggregate matches the requested _amount, and nothing in StoneVault caps the value afterward.
The critical invariant is straightforward: for any instant withdrawal, ETH transferred out of StoneVault must be less than or equal to the caller's share-based entitlement. The code-level breakpoint is the addition of the unchecked actualAmount returned from controller.forceWithdraw(ethAmount) inside StoneVault.instantWithdraw().
4. Detailed Root Cause Analysis
The relevant victim-side code from the verified StakeStone source is:
Verified StoneVault::instantWithdraw logic:
uint256 ethAmount = VaultMath.sharesToAsset(_shares, sharePrice);
stoneMinter.burn(msg.sender, _shares);
if (ethAmount <= idleAmount) {
actualWithdrawn = actualWithdrawn + ethAmount;
} else {
actualWithdrawn = actualWithdrawn + idleAmount;
ethAmount = ethAmount - idleAmount;
StrategyController controller = StrategyController(strategyController);
uint256 actualAmount = controller.forceWithdraw(ethAmount);
actualWithdrawn = actualWithdrawn + actualAmount;
}
Verified StrategyController::forceWithdraw and _forceWithdraw logic:
function forceWithdraw(uint256 _amount) external onlyVault returns (uint256 actualAmount) {
uint256 balanceBeforeRepay = address(this).balance;
if (balanceBeforeRepay >= _amount) {
_repayToVault();
actualAmount = balanceBeforeRepay;
} else {
actualAmount =
_forceWithdraw(_amount - balanceBeforeRepay) +
balanceBeforeRepay;
}
}
function _forceWithdraw(uint256 _amount) internal returns (uint256 actualAmount) {
uint256 length = strategies.length();
for (uint i; i < length; i++) {
address strategy = strategies.at(i);
uint256 withAmount = (_amount * ratios[strategy]) / ONE_HUNDRED_PERCENT;
if (withAmount != 0) {
actualAmount =
Strategy(strategy).instantWithdraw(withAmount) +
actualAmount;
}
}
_repayToVault();
}
The bug is the absence of a boundary check after forceWithdraw() returns. StoneVault computes ethAmount as the caller's legitimate shortfall, but then pays actualAmount, which is only a downstream strategy return value and may be larger than ethAmount.
The incident trace proves that this exact path was taken. Human-labeled excerpt from the seed transaction trace:
StoneVault::instantWithdraw(0, 8582162020025013545654)
...
StrategyController::forceWithdraw(8579775372585181673317)
...
AssetsVault::receive{value: 8617073005336909691789}()
emit WithdrawnFromStrategy(
param0: 0x9C52c485EdD3D22847A1614B8988Fbf520b33047,
param1: 8579775372585181673317,
param2: 8617073005336909691789,
param3: 4
)
AssetsVault::withdraw(0x9C52c485EdD3D22847A1614B8988Fbf520b33047, 8617073005336909691789)
param1 is the requested strategy shortfall and param2 is the actual returned amount accepted by the vault. Their difference is:
8617073005336909691789
-8579775372585181673317
= 37297632751728018472 wei
That 37.297632751728018472 ETH delta is the deterministic over-withdrawal caused by the uncapped return path. The exploit conditions were public and permissionless at the transaction pre-state: the attacker only needed public flash liquidity, public StoneVault entrypoints, and the live strategy configuration already deployed by the protocol.
5. Adversary Flow Analysis
The adversary realized the bug in a single transaction:
- EOA
0x9abe851bcc4fd1986c3d1ef8978fad86a26a0c57called attacker contract0x9c52c485edd3d22847a1614b8988fbf520b33047. - The attacker contract used
AavePool.flashLoanSimple()to borrow8600 WETH, then unwrapped it to ETH. - It deposited
8600 ETHintoStoneVault, minting8582.162020025013545654 STONE. - It called
rollToNextRound(), which moved8600.005 ETHinto StakeStone's strategies and advanced the vault from round3to round4with share price0.999721906037865163. - It approved the freshly minted
STONEand calledinstantWithdraw(0, 8582162020025013545654). - During that withdrawal,
StoneVaultcomputed a strategy shortfall of8579.775372585181673317 ETH, but the controller returned8617.073005336909691789 ETHfrom strategies and the vault paid all of it to the attacker contract. - The attacker rewrapped enough ETH to repay the flash loan plus the
4.3 ETHAave premium, leaving3 ETHat the receiver contract. The sender EOA separately paid0.119363317574586483 ETHin gas.
This flow aligns with the three lifecycle stages in the underlying analysis:
- Flash-loan funding through Aave.
- STONE mint plus public rebalance through
rollToNextRound(). - Over-withdrawal through
instantWithdraw()and repayment of the flash loan.
6. Impact & Losses
The direct protocol accounting loss is 37.297632751728018472 ETH, encoded on-chain as "37297632751728018472". That value is the excess returned by strategies over the caller's legitimate share-based entitlement.
The attacker cluster's realized profit was smaller because the exploit also had to cover the 4.3 ETH Aave premium and the sender EOA's gas spend. The receiver contract ended with exactly 3 ETH, and the sender EOA lost 0.119363317574586483 ETH in native balance for gas, leaving a net cluster gain of 2.880636682425413517 ETH after gas. Even so, the underlying vault loss is the larger 37.297632751728018472 ETH overpayment extracted from StakeStone.
The violated security principles are:
- Vault payouts must be capped by explicit user entitlement, not by unchecked downstream return values.
- Cross-module accounting assumptions must be validated at module boundaries.
7. References
- Incident transaction:
0xbf5b2d22fa88965ddfc6e6d685fc7cfc683340c49e126386759ed9e4027b1415 - Etherscan transaction page: https://etherscan.io/tx/0xbf5b2d22fa88965ddfc6e6d685fc7cfc683340c49e126386759ed9e4027b1415
- Victim contract
StoneVault:0xa62f9c5af106feee069f38de51098d9d81b90572 - Victim contract
StrategyController:0x396abf9ff46e21694f4ef01ca77c6d7893a017b2 - Collected StakeStone source package:
/workspace/session/artifacts/collector/seed/1/0x7122985656e38bdc0302db86685bb972b145bd3c/ - Seed transaction trace:
/workspace/session/artifacts/collector/seed/1/0xbf5b2d22fa88965ddfc6e6d685fc7cfc683340c49e126386759ed9e4027b1415/trace.cast.log - Seed balance diff:
/workspace/session/artifacts/collector/seed/1/0xbf5b2d22fa88965ddfc6e6d685fc7cfc683340c49e126386759ed9e4027b1415/balance_diff.json