Midas LP Oracle Read-Only Reentrancy via Curve stMATIC/WPOL
Exploit Transactions
0x0053490215baf541362fc78be0de98e3147f40223238d5b12512b3e26c0a2c2fVictim Addresses
0xD265ff7e5487E9DD556a4BB900ccA6D087Eb3AD2Polygon0x23F43c1002EEB2b146F286105a9a2FC75Bf770A4Polygon0xFb6FE7802bA9290ef8b00CA16Af4Bc26eb663a28PolygonLoss Breakdown
Similar Incidents
0VIX ovGHST Oracle Inflation
35%dForce Oracle Reentrancy Liquidation
32%BonqDAO ALBT Oracle Manipulation via TellorFlex
31%Paribus Redeem Reentrancy
27%LunaFi VLFI Reward Replay
27%Polygon Uninitialized Clone Wallet Takeover and TEL Drain
24%Root Cause Analysis
Midas LP Oracle Read-Only Reentrancy via Curve stMATIC/WPOL
1. Incident Overview TL;DR
On Polygon block 38118348, exploit transaction 0x0053490215baf541362fc78be0de98e3147f40223238d5b12512b3e26c0a2c2f used public Balancer and Aave flash-loan liquidity to manipulate the collateral price of Midas market 0x23F43c1002EEB2b146F286105a9a2FC75Bf770A4 (fWMATIC_STMATIC). The attacker first deposited Curve stMATIC/WPOL LP as collateral, then triggered a native-token callback inside Curve pool 0xFb6FE7802bA9290ef8b00CA16Af4Bc26eb663a28 and reentered Midas while the LP oracle was transiently overstated. During that callback window, the attacker borrowed four Jarvis jFIAT markets against the inflated collateral and then self-liquidated the position after the oracle normalized. The attacker contract 0x757e9f49acfab73c25b20d168603d54a66c723a1 finished the transaction with 663101.518942206398329551 native MATIC, while the sender EOA paid 1.2065462 MATIC in gas.
The root cause is a read-only reentrancy across Curve and Midas. Curve remove_liquidity(..., use_eth=true) burns LP supply and transfers native value before it refreshes cached invariant self.D. Midas prices the LP collateral through an oracle path that consumes Curve get_virtual_price() live during borrow checks. As a result, Midas observed a stale invariant divided by an already-reduced LP supply and temporarily valued the LP at about 10.091e18 instead of about 1.002e18, which let the attacker borrow against collateral value that did not exist in finalized pool state.
2. Key Background
Midas uses Compound/Fuse-style liquidity checks. For each collateral market, the comptroller computes effective collateral from the cToken balance, exchangeRateStored(), the market collateral factor, and the oracle price returned for the underlying asset. For the affected market, the underlying asset was Curve LP token 0xe7CEA2F6d7b120174BF3A9Bc98efaF1fF72C997d, and the oracle price came from a dedicated LP-oracle path rather than from a direct single-token feed.
The key contracts in the pricing stack are:
- Midas Comptroller:
0xD265ff7e5487E9DD556a4BB900ccA6D087Eb3AD2 - LP collateral market:
0x23F43c1002EEB2b146F286105a9a2FC75Bf770A4 - LP oracle proxy:
0xaCF3E1C6f2D6Ff12B8aEE44413D6834774B3f7A3 - Exploit-time LP oracle implementation:
0x3803527dcd92Ac3e72A0A164Db82734DABa47Fac - Recursive price oracle:
0x7ef2a6a6af8ee090da3a24ea947b3717c4d16d94 - Curve stMATIC/WPOL pool:
0xFb6FE7802bA9290ef8b00CA16Af4Bc26eb663a28
The collected LP-oracle implementation note shows that getUnderlyingPrice() recursively prices the LP constituents and then reads the pool's live virtual price:
function getUnderlyingPrice(address arg0) public payable {
(bool success, bytes memory ret0) = address(arg0).underlying(); // staticcall
...
(bool success, bytes memory ret0) = address(msg.sender).Unresolved_aea91078(var_h); // price(underlying)
...
(bool success, bytes memory ret0) = address(storage_map_j[var_d]).get_virtual_price(); // staticcall
}
The recursive price oracle itself stayed stable during the exploit window. The collected block-38118347 direct reads show price(WPOL) = 1e18 and price(stMATIC) = 1056568563958165728. That isolates the exploitable price movement to the Curve LP virtual-price path rather than to any change in underlying-token feeds.
The adversary cluster is also trace-backed:
- EOA
0x1863b74778cf5e1c9c482a1cdc2351362bd08611deployed attack contract0x757e...in tx0xe709b50b92e9cb79d143c196505e2139d5ae765bb975ca5ac87f62dbc412c62c. - The exploit transaction called
0x757e...directly. - Helper liquidator
0x97c347bacbd37d853c98de415fce3f459908b4f5was created during the exploit transaction by the attacker contract.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK-class ACT exploit. The safety invariant is that the LP price consumed by Midas during borrowAllowed must reflect finalized Curve pool state. For an LP collateral market priced through get_virtual_price(), cached invariant self.D, pool balances, and LP total supply must all refer to the same post-transition state whenever the oracle is externally observable.
Curve violated that invariant along the native withdrawal path. The verified pool source shows that remove_liquidity burns LP, transfers native value to the receiver, and only then updates self.D. The same source shows that get_virtual_price() divides an invariant-derived quantity by the current LP total supply. Once LP supply has already been burned, but self.D still refers to the pre-withdrawal state, the virtual price becomes overstated.
Midas then turned that transient inconsistency into borrowable collateral value by trusting the live pool read inside its liquidity checks. The focused trace proves the distortion was not permanent: the LP price was normal before the callback, spiked only during the callback, and returned near parity after the callback completed. That behavior is precisely the signature of a read-only reentrancy oracle bug rather than a lasting repricing or a constituent-feed issue.
Because the attacker regained control during the callback and reentered Midas before Curve refreshed self.D, the exploit was deterministic. The attacker could borrow against collateral value that existed only in the inconsistent mid-withdrawal state, then realize the resulting bad debt through self-liquidation once the price normalized.
4. Detailed Root Cause Analysis
4.1 ACT Pre-State
The ACT opportunity is defined at Polygon block 38118347, immediately before the exploit transaction. At that point:
- public Balancer and Aave pools exposed enough WMATIC flash liquidity to finance the exploit;
- Midas accepted the Curve LP as collateral in
fWMATIC_STMATIC; - the LP-oracle proxy delegated to implementation
0x3803...and mapped the LP to the Curve stMATIC/WPOL pool; - the Curve pool exposed the native-token callback path through
use_eth=true.
The seed artifacts confirm that the pre-state LP price was normal. The seed evidence and focused oracle trace place the pre-state collateral price at about 1002157321772769944, close to parity.
4.2 Code-Level Breakpoint
The critical Curve withdrawal logic is:
lp_token: address = self.token
total_supply: uint256 = CurveToken(lp_token).totalSupply()
CurveToken(lp_token).burnFrom(msg.sender, _amount)
...
if use_eth and coin == WETH20:
raw_call(receiver, b"", value=d_balance)
...
D: uint256 = self.D
self.D = D - D * amount / total_supply
The corresponding pricing logic is:
def get_virtual_price() -> uint256:
return 10**18 * self.get_xcp(self.D) / CurveToken(self.token).totalSupply()
That ordering creates the exploit window. During the callback, LP supply is already lower, but self.D is still stale. Any external protocol that reads get_virtual_price() during that window receives an inflated LP price.
4.3 Oracle Trace Evidence
The focused oracle-call trace records the exact pricing sequence:
Pre-callback:
getUnderlyingPrice(fWMATIC_STMATIC) = 1002157321772769944
Curve.get_virtual_price() = 1002157321772769944
During callback:
getUnderlyingPrice(fWMATIC_STMATIC) = 10091002696492234934
Curve.get_virtual_price() = 10091002696492234934
Post-callback:
getUnderlyingPrice(fWMATIC_STMATIC) = 1002197920733900715
Curve.get_virtual_price() = 1002197920733900715
The same trace shows that the constituent prices remained stable during the exploit:
WPOL price = 1000000000000000000
stMATIC price = 1056568563958165728
So the exploitable price movement came entirely from the LP virtual price, not from any feed movement in the underlying assets.
4.4 Borrow Window, Liquidation, and Profit Realization
The attacker first primed the Midas account by minting LP and depositing it into fWMATIC_STMATIC. It then performed a much larger one-sided Curve add/remove cycle to open the callback window. The collected evidence records:
- initial LP minted for collateral:
131863.795920435857673899 - initial cTokens minted in Midas:
659318.979602179288369495 - large roundtrip LP minted:
22566542.130775321564334700 - callback-time native MATIC sent from Curve to the attacker:
68074207.302427837361065300
During that callback, the attacker borrowed from four Jarvis markets:
296187.577226634081924767jCHF425500jEUR50000jGBP50204.765122889316550500EURA
When Curve later refreshed self.D, the LP price normalized and the Midas position became undercollateralized. The attacker then created helper 0x97c347..., self-liquidated, seized 513648.897590228880099075 collateral cTokens, redeemed 102729.779518045776019814 LP, swapped the proceeds, and exited in native MATIC.
5. Adversary Flow Analysis
The adversary flow consisted of one setup transaction and one exploit transaction:
-
Setup deployment
- Tx
0xe709b50b92e9cb79d143c196505e2139d5ae765bb975ca5ac87f62dbc412c62c - Block
38118251 - The attacker EOA deployed exploit contract
0x757e9f49acfab73c25b20d168603d54a66c723a1.
- Tx
-
Exploit execution
- Tx
0x0053490215baf541362fc78be0de98e3147f40223238d5b12512b3e26c0a2c2f - Block
38118348 - The attack contract drew WMATIC flash liquidity from Balancer, Aave v3, and Aave v2.
- It minted LP, entered the Midas markets, and posted LP as collateral.
- It performed the large Curve
add_liquidity/remove_liquidity(..., true)cycle. - Inside the callback it reentered Midas and opened the four Jarvis borrows.
- After normalization it deployed helper
0x97c347..., liquidated the position, redeemed seized collateral, swapped assets back to WMATIC, and withdrew to native MATIC.
- Tx
This remained a permissionless ACT path throughout. The helper contract was created by the attacker during the exploit transaction itself and did not provide any privileged capability beyond what any adversary could reproduce.
6. Impact & Losses
The measurable impact is the conversion of transiently overstated collateral into undercollateralized debt across four Jarvis markets, followed by profit realization at the attacker contract.
The seed balance diff shows:
- attacker contract
0x757e...native MATIC delta:+663101518942206398329551wei - sender EOA native MATIC delta:
-1206546200000000000wei
The lasting market losses recorded in the seed balance diff were:
| Token | Raw Delta | Display Delta |
|---|---|---|
| jCHF | -273973508934636525780410 | 273973.508934636525780410 |
| jEUR | -368057500000000000000000 | 368057.500000000000000000 |
| jGBP | -45250000000000000000000 | 45250.000000000000000000 |
| EURA | -45435312436214831478203 | 45435.312436214831478203 |
The native-MATIC profit is the attacker-side realization of that bad debt after liquidation and swaps. The protocol-level impact is broader than the profit amount: Midas accepted collateral priced at roughly 10x its finalized value and therefore authorized borrows that should never have passed liquidity checks.
7. References
- Setup deployment tx:
0xe709b50b92e9cb79d143c196505e2139d5ae765bb975ca5ac87f62dbc412c62c - Exploit tx:
0x0053490215baf541362fc78be0de98e3147f40223238d5b12512b3e26c0a2c2f - Seed tx metadata and seed balance diff for the exploit transaction
- Focused oracle trace showing pre-callback, callback-time, and post-callback LP prices
- LP-oracle collector note for implementation
0x3803527dcd92ac3e72a0a164db82734daba47fac - Recursive-price-oracle collector note for
0x7ef2a6a6af8ee090da3a24ea947b3717c4d16d94 - Attacker recent-activity and contract-creation summary linking
0x1863...,0x757e..., and0x97c347... - Auditor evidence note containing the verified Curve and Midas code excerpts and exploit-time quantities