LibertiVault Reentrant Share Inflation
Exploit Transactions
0x7320accea0ef1d7abca8100c82223533b624c82d3e8d445954731495d4388483Victim Addresses
0x9c80a455ecaca7025a45f5fa3b85fd6a462a447bPolygonLoss Breakdown
Similar Incidents
Flashstake LP Share Inflation
38%0VIX ovGHST Oracle Inflation
35%SimpleSwap Reserve Drain on Polygon
30%Midas LP Oracle Read-Only Reentrancy via Curve stMATIC/WPOL
30%LunaFi VLFI Reward Replay
27%Telcoin Wallet Reinitialization Drain
27%Root Cause Analysis
LibertiVault Reentrant Share Inflation
1. Incident Overview TL;DR
On Polygon transaction 0x7320accea0ef1d7abca8100c82223533b624c82d3e8d445954731495d4388483, an unprivileged adversary used an attacker-controlled helper contract, an Aave V2 flash loan, and the public 1inch executor callback path to reenter LibertiVault.deposit() before the outer deposit finished minting shares. The exploit targeted LibertiVault at 0x9c80a455ecaca7025a45f5fa3b85fd6a462a447b and was fully realizable from public state at block 44941584, where the vault already had live WETH/USDT balances and non-zero share supply.
The root cause is an inconsistent accounting snapshot inside the vault’s deposit path. deposit() snapshots NAV before the external swap, but _deposit() reads totalSupply() only after the attacker-controlled 1inch callback returns. That lets the callback reenter deposit(), mint a large inner share position first, and then lets the outer deposit compute shares with a stale denominator and an inflated numerator. The attacker then calls exit() to redeem the inflated shares against the vault’s real assets, leaving the vault short 129399932660161219914 wei of WETH and 54812691331 raw USDT.
2. Key Background
LibertiVault is a dual-asset vault that holds WETH as asset and USDT as other. At the vulnerable pre-state, the vault was configured with invariant = 7473, entryFee = 10, exitFee = 30, minDeposit = 1000000000000000, pre-state share supply 21062278834564420093169, WETH balance 129411606107394699466, and USDT balance 81476027164. Because the invariant was below 10000, deposits were required to rebalance part of the incoming WETH through 1inch before final share minting.
That swap path is reached through LibertiAggregationRouterV4.userSwap(), which decodes attacker-supplied calldata and forwards it to the public 1inch Aggregation Router V4 at 0x1111111254fb6c44bac0bed2854e76f90643097d. On Polygon, 1inch’s swap(...) entrypoint can invoke an attacker-specified executor contract. In the attack transaction, the executor callback target was the attacker helper contract 0xdfcdb5a86b167b3a418f3909d6f7a2f2873f2969.
exit() is economically important because it lets any share holder burn their shares and withdraw their pro-rata claim on both underlying tokens without needing another off-chain quote. Once the attacker inflates shares, exit() converts that accounting inflation into immediately withdrawable WETH and USDT.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK, not an MEV-only repricing event. The exploitable invariant is simple: for any deposit, the share mint formula must use one consistent state boundary for both the NAV denominator and the share-supply numerator. LibertiVault violates that invariant by snapshotting NAV before an external call and reading totalSupply() only after the external call returns.
Victim contract source, condensed:
function deposit(uint256 assets, address receiver, bytes calldata data)
external
returns (uint256 shares)
{
uint256 nav = getNavInNumeraire(MathUpgradeable.Rounding.Up);
SafeERC20Upgradeable.safeTransferFrom(asset, _msgSender(), address(this), assets);
shares = _deposit(assets, receiver, data, nav);
}
function _deposit(uint256 assets, address receiver, bytes calldata data, uint256 nav)
private
returns (uint256 shares)
{
if (BASIS_POINT_MAX > invariant) {
returnAmount = userSwap(data, address(this), swapAmount, address(asset), address(other));
}
uint256 supply = totalSupply();
shares = supply.mulDiv(valueToken0 + valueToken1, nav, MathUpgradeable.Rounding.Down);
_mint(receiver, shares - feeAmount);
}
The code-level breakpoint is the gap between nav = getNavInNumeraire(...) and the later uint256 supply = totalSupply(); after userSwap(...). Because userSwap() reaches the public 1inch router with attacker-controlled calldata, the attacker can reenter deposit() during the callback, expand supply, and return to the outer deposit with a larger numerator than the one that matched the original NAV snapshot. The result is over-minted shares and a broken share-accounting invariant.
The vulnerable components are LibertiVault.deposit(), LibertiVault._deposit(), LibertiVault.exit(), and the routing wrapper LibertiAggregationRouterV4.userSwap()/ _swap(). The security principles violated are equally direct: external calls occur during critical share-mint accounting, supply and NAV are not snapshotted from the same state boundary, and user-controlled swap callbacks are trusted inside a sensitive mint path.
4. Detailed Root Cause Analysis
The pre-state at block 44941584 already satisfied the exploit conditions: the vault had a live share supply, non-zero WETH and USDT reserves, and invariant = 7473, so deposits would take the external swap path. The attacker only needed temporary liquidity and control of the public callback path. Aave V2 supplied the temporary USDT liquidity via flashLoan, and the 1inch executor callback provided the reentrancy hook.
The seed trace shows the decisive call sequence:
0x8dFf5E27EA6b7AC08EbFdf9eB090F32ee9a30fcf::flashLoan(...)
0xdFcDB5A86b167B3A418F3909D6f7A2f2873F2969::executeOperation(...)
0x1111111254fb6c44bAC0beD2854e76F90643097d::swap(...)
0xdFcDB5A86b167B3A418F3909D6f7A2f2873F2969::2636f7f8(...)
LibertiVault::deposit(...)
emit Deposit(... shares: 745946560805792436718600)
emit Deposit(... shares: 21106759427094646036445794)
LibertiVault::exit()
emit Exit(... assets: 2215634869835882801, stable: 5060734454518, shares: 21765360722066800420587158)
The first critical fact is that the inner callback reaches LibertiVault.deposit() while the outer deposit is still active. The second critical fact is magnitude: the two traced deposits mint 745946560805792436718600 shares and 21106759427094646036445794 shares, while the entire pre-state vault supply was only 21062278834564420093169. That is not a normal deposit scale-up; it is direct evidence that the reentrant path distorted share minting far beyond the legitimate pre-state capitalization of the vault.
Mechanically, the exploit works as follows. The outer deposit records NAV before minting. It then transfers in the attacker’s WETH and enters _deposit(). Because invariant < 10000, _deposit() computes a swap amount and calls userSwap(). userSwap() forwards attacker-supplied calldata into the public 1inch router, which invokes the attacker helper as executor. Inside that callback, the helper reenters deposit() with another minimum-size WETH deposit. The inner deposit completes first and materially increases totalSupply(). Control then returns to the outer deposit, which still uses the stale outer nav but now reads the inflated post-reentrancy totalSupply(). That mixed-state formula amplifies the outer mint and hands the attacker a share balance that should never exist under a consistent snapshot.
Once the attacker holds the inflated shares, exit() realizes the theft. The trace shows exit() transferring 2215634869835882801 wei of WETH and 5060734454518 raw USDT to the attacker helper before flash-loan repayment. The balance diff then confirms the post-state effect on the real vault balances: WETH falls from 129411606107394699466 to 11673447233479552, and USDT falls from 81476027164 to 26663335833. Those losses match the share-inflation theory and are exactly what a successful over-mint followed by redemption should produce.
5. Adversary Flow Analysis
- The sender EOA
0xfd2d3ffb05ad00e61e3c8d8701cb9036b7a16d02invoked its helper contract0xdfcdb5a86b167b3a418f3909d6f7a2f2873f2969in Polygon block44941585. - The helper borrowed
5000000000000raw USDT from Aave V2 and also sourced the required temporary WETH exposure needed to complete the vault deposit path. - During
executeOperation(...), the helper approved the vault, built 1inch swap calldata that pointed the public executor callback back to itself, and calledLibertiVault.deposit(...). - When 1inch invoked the helper callback, the helper reentered
LibertiVault.deposit(...), causing the inner mint to occur before the outer deposit finalized. The helper then recorded the temporary peak share balance and immediately calledexit()to convert the inflated shares into underlying assets. - After
exit(), the helper repaid Aave principal plus the4500000000raw USDT premium, swapped enough WETH to cover any USDT shortfall, transferred123839918184977796314wei of WETH to the sender EOA, and retained56234454518raw USDT on the helper contract.
This sequence is fully permissionless. No private keys, privileged roles, or hidden state were required beyond what any unprivileged contract could do with public Aave flash liquidity and public 1inch routing.
6. Impact & Losses
The economically dominant impact was near-total WETH depletion from the vault, plus a large USDT loss. Remaining shareholders were left with a nearly empty vault after the attacker realized the inflated shares through exit().
| Asset | Before | After | Loss |
|---|---|---|---|
| WETH | 129411606107394699466 | 11673447233479552 | 129399932660161219914 |
| USDT | 81476027164 | 26663335833 | 54812691331 |
The adversary cluster’s final retained balances were 123839918184977796314 wei of WETH on the sender EOA and 56234454518 raw USDT on the helper contract. Those values are net of flash-loan repayment and therefore satisfy the ACT profit predicate directly from state deltas, without any off-chain price assumption.
7. References
- Attack transaction:
0x7320accea0ef1d7abca8100c82223533b624c82d3e8d445954731495d4388483on Polygon block44941585. - Victim contract:
LibertiVaultat0x9c80a455ecaca7025a45f5fa3b85fd6a462a447b. - Public routing dependency: 1inch Aggregation Router V4 at
0x1111111254fb6c44bac0bed2854e76f90643097d. - Public liquidity dependency: Aave V2 LendingPool at
0x8dff5e27ea6b7ac08ebfdf9eb090f32ee9a30fcf. - Pre-state evidence: collected block-
44941584vault observations showing invariant, fees, supply, and token balances. - Execution evidence: collected trace for tx
0x7320...showingflashLoan,executeOperation, nesteddeposit, oversizedDepositevents, andexit. - Post-state evidence: collected balance-diff artifact for tx
0x7320...showing vault losses and attacker-cluster profit. - Public source references: Polygonscan verified source for LibertiVault and the public 1inch router code at the addresses above.