OMPxContract bonding-curve loop exploit drains ETH reserves
Exploit Transactions
0xd927843e30c6b2bf43103d83bca6abead648eac3cad0d05b1b0eb84cd87de9b6Victim Addresses
0x09a80172ed7335660327cd664876b5df6fe06108EthereumLoss Breakdown
Similar Incidents
AnyswapV4Router WETH9 permit misuse drains WETH to ETH
36%PLNTOKEN transferFrom burn hook drains WETH reserves
35%Revest TokenVault withdrawFNFT accounting flaw drains RENA vault reserves
34%FlippazOne ungated ownerWithdrawAllTo lets attacker drain 1.15 ETH
34%Access-control bug draining 5 ETH from token contract
34%TRU reserve mispricing attack drains WBNB from pool
33%Root Cause Analysis
OMPxContract bonding-curve loop exploit drains ETH reserves
1. Incident Overview TL;DR
On Ethereum mainnet block 20468780, EOA 0x40d115198d71cab59668b51dd112a07d273d5831 used a freshly deployed helper contract 0xfaddf57d079b01e53d1fe3476cc83e9bcc705854 plus Balancer/WETH liquidity to execute a single-transaction looping strategy against OMPxContract 0x09a80172ed7335660327cd664876b5df6fe06108.
The helper borrowed 100 WETH from the Balancer Vault, unwrapped it to 100 ETH, and repeatedly interacted with OMPxContract in a tight loop that combined large purchase and buyBack calls. Each loop iteration transferred nearly the full 100 ETH back out of OMPxContract while reclaiming the same large OMPxToken amount, producing a small per-iteration ETH shortfall.
Over dozens of iterations in the same transaction, these shortfalls accumulated into a net loss of 4.372869914943298279 ETH from OMPxContract. After repaying Balancer principal and fees and paying gas, the adversary-controlled cluster (EOA + helper) realized a net profit of approximately 4.371401571200395847 ETH.
2. Key Background
OMPxContract is an owner-controlled bonding-curve style contract that mints and burns OMPxToken (0x633b041c41f61d04089880d7b5c7ed0f10ff6f85) against ETH reserves. The core design goal is that the ETH reserves held by OMPxContract track the outstanding OMPxToken supply along a monotonic price curve.
Two public pricing functions underpin this design:
// OMPxContract (0x09a80172...6108), pricing core
function getBuyBackPrice(uint256 buyBackValue) public view returns (uint256 price_) {
if (address(this).balance == 0) return 0;
uint256 eth;
uint256 tokens = token.totalSupply();
if (buyBackValue > 0) {
eth = address(this).balance.sub(buyBackValue);
} else {
eth = address(this).balance;
}
return (eth.sub(feeBalance)).mul(1e18).div(tokens);
}
function getPurchasePrice(uint256 purchaseValue, uint256 amount) public view returns (uint256 price_) {
uint256 buyerContributionCoefficient = getDiscountByAmount(amount);
uint256 price = getBuyBackPrice(purchaseValue).mul(buyerContributionCoefficient).div(descPrecision);
if (price <= 0) { price = 1e11; }
return price;
}
getBuyBackPrice uses address(this).balance (optionally reduced by buyBackValue) and token.totalSupply() to compute a per-token price in ETH, net of feeBalance. getPurchasePrice reuses this buyback price and applies a discount coefficient based on the amount being purchased.
User-facing flows then combine these prices with token mint/burn and ETH transfers:
// purchase: user sends ETH and receives tokens
function purchase(uint256 tokensToPurchase, uint256 maxPrice) public payable returns (uint256 tokensBought_) {
uint256 currentPrice = getPurchasePrice(msg.value, tokensToPurchase);
// ...
token.safeTransfer(msg.sender, tokensWuiAvailableByCurrentPrice);
// partial ETH refund if msg.value > totalDealPrice
}
// buyBack: user returns tokens and receives ETH
function buyBack(uint256 tokensToBuyBack, uint256 minPrice) public {
uint currentPrice = getBuyBackPrice(0);
uint256 totalPrice = tokensToBuyBack.mul(currentPrice).div(1e18);
token.safeTransferFrom(msg.sender, this, tokensToBuyBack);
msg.sender.transfer(totalPrice);
}
For the system to be safe, iterating combinations of purchase and buyBack should preserve an accounting invariant: after any complete cycle, OMPxContract’s ETH reserves (net of feeBalance) must remain at least as large as the ETH value implied by the outstanding OMPxToken supply at the curve price.
Balancer’s Vault (0xba12222222228d8ba445958a75a0704d566bf2c8) and WETH (0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) provide permissionless flash-loan-like liquidity: any contract can temporarily borrow WETH, unwrap it to ETH, use that ETH to interact with other contracts, and rewrap/repay within the same transaction as long as the borrowed amount plus fees are returned. The adversary uses this to source 100 ETH of temporary capital without prior holdings.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is a loop-unsafe bonding-curve accounting bug in OMPxContract’s pricing logic. Both purchase and buyBack ultimately rely on getBuyBackPrice, which uses the current address(this).balance and token.totalSupply() to derive a price. However:
purchasecallsgetPurchasePrice(msg.value, tokensToPurchase), which in turn callsgetBuyBackPrice(purchaseValue)using the incoming ETH amount asbuyBackValue. This effectively computes prices using a synthetic ETH reserveaddress(this).balance - msg.valuerather than the post-trade reserve.buyBackcallsgetBuyBackPrice(0), using the current on-chain balance with no adjustment for pending intra-transaction flows.
When an attacker-controlled contract can rapidly alternate purchase and buyBack in a single transaction with a fixed, very large token amount, these price functions are repeatedly evaluated on stale or partially updated ETH balances. The resulting small per-iteration mismatch between the ETH paid in and ETH paid out accumulates, allowing the attacker to extract value from reserves without increasing net token supply.
The concrete invariant is:
Let
ETH_reservebe OMPxContract’s ETH balance net offeeBalance, andP(totalSupply)be the bonding-curve price per token. After any completepurchase/buyBacklifecycle,
ETH_reserve >= P(totalSupply) * totalSupply / 1e18
must hold.
In tx 0xd927...de9b6, the helper contract repeatedly calls OMPxContract with selectors:
0x70876c98(purchase(uint256,uint256)) with 100 ETH value.0x47d8167d(buyBack(uint256,uint256)).
As evidenced by the trace, this loop causes OMPxContract’s ETH_reserve to fall below the value implied by the outstanding token supply, violating the invariant and leaking 4.372869914943298279 ETH from the contract into the adversary’s control.
4. Detailed Root Cause Analysis
4.1 Code-level mechanism
The core accounting mechanism is:
- Prices depend on
address(this).balanceandtoken.totalSupply(). getBuyBackPrice(buyBackValue)subtractsbuyBackValuefrom the current ETH balance (if non-zero) before computing price.getPurchasePrice(purchaseValue, amount)callsgetBuyBackPrice(purchaseValue)using the user’s ETH contribution asbuyBackValue, then applies a discount.
This induces an asymmetric treatment of ETH flows when purchase and buyBack are interleaved in a single transaction. In particular:
- Before any exploit loop, OMPxContract holds some ETH reserve
R0and totalSupplyS. - In a
purchasecall, the contract evaluates prices usingeth = R0 - 100 ETH(conceptually) viagetBuyBackPrice(msg.value), while the actual post-trade ETH balance will be closer toR0 + 100 ETH - fee - refund. This mismatch allows purchase of a very large token amount at a price computed from a lower effective reserve. - In a subsequent
buyBackwith the same large token amount,getBuyBackPrice(0)uses the higher on-chain ETH balance but the same supplyS, leading to an over-generous buyback payout relative to the earlier purchase cost. - Repeating this
purchase/buyBackpair with carefully chosen parameters produces a small ETH surplus per iteration, which can be compounded by looping while the contract’s state remains within acceptable bounds forrequirechecks.
4.2 Evidence from OMPxContract source
The relevant binding between ETH reserves, token supply, and prices is visible in the verified source:
// OMPxContract: buyback pricing and ETH payout
function buyBack(uint256 tokensToBuyBack, uint256 minPrice) public {
uint currentPrice = getBuyBackPrice(0);
require(currentPrice >= minPrice);
uint256 totalPrice = tokensToBuyBack.mul(currentPrice).div(1e18);
// ...
token.safeTransferFrom(msg.sender, this, tokensToBuyBack);
// send out eth
msg.sender.transfer(totalPrice);
}
The contract does not implement any reentrancy guard (nonReentrant) or per-transaction caps on token amounts or ETH flows for purchase or buyBack. As long as each individual call satisfies the require checks, an external helper contract is free to sequence them arbitrarily within one transaction.
4.3 Evidence from on-chain trace
The QuickNode callTracer trace for tx 0xd927843e30c6b2bf43103d83bca6abead648eac3cad0d05b1b0eb84cd87de9b6 shows the precise looping pattern. A representative excerpt:
// Seed tx call trace excerpt (tx 0xd927...de9b6)
{
"from": "0xba12222222228d8ba445958a75a0704d566bf2c8",
"to": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"type": "CALL",
"input": "0xa9059cbb...faddf57d079b01e53d1fe3476cc83e9bcc705854...",
"value": "0x0"
},
{
"from": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"to": "0xfaddf57d079b01e53d1fe3476cc83e9bcc705854",
"type": "CALL",
"value": "0x56bc75e2d63100000" // 100 WETH -> helper as ETH
},
{
"from": "0xfaddf57d079b01e53d1fe3476cc83e9bcc705854",
"to": "0x09a80172ed7335660327cd664876b5df6fe06108",
"type": "CALL",
"input": "0x70876c98...017d15453a5c62f040c5b4...0000000000000000000009184e72a000",
"value": "0x56bc75e2d63100000" // purchase with 100 ETH
},
{
"from": "0x09a80172ed7335660327cd664876b5df6fe06108",
"to": "0xfaddf57d079b01e53d1fe3476cc83e9bcc705854",
"type": "CALL",
"value": "0x50dc43a752c1b36f4" // ETH back to helper
}
Later in the same transaction, the helper invokes buyBack repeatedly:
// Looping buyBack calls with same token amount
{
"from": "0xfaddf57d079b01e53d1fe3476cc83e9bcc705854",
"to": "0x09a80172ed7335660327cd664876b5df6fe06108",
"type": "CALL",
"input": "0x47d8167d...017d15453a5c62f040c5b4...00000000000000000000000000000001",
"value": "0x0"
},
{
"from": "0x09a80172ed7335660327cd664876b5df6fe06108",
"to": "0xfaddf57d079b01e53d1fe3476cc83e9bcc705854",
"type": "CALL",
"value": "0x80f1cddb401fa696" // ETH payout for buyBack
}
Etherscan’s internal txlist for OMPxContract over block 20468780 compresses this into multiple internal transfers where:
- Helper sends exactly
100 ETHto OMPxContract multiple times. - OMPxContract sends slightly less than
100 ETHback each time, in varying amounts.
The prestate balance diff confirms the net effect:
// prestateTracer native balance deltas for tx 0xd927...de9b6
{
"address": "0x09a80172ed7335660327cd664876b5df6fe06108",
"delta_wei": "-4372869914943298279"
},
{
"address": "0x40d115198d71cab59668b51dd112a07d273d5831",
"delta_wei": "4371401571200395847"
}
OMPxContract loses exactly 4.372869914943298279 ETH, and the EOA gains 4.371401571200395847 ETH over this single transaction (the small difference is gas/miner-related). No other contract shows a correspondingly large negative delta, confirming that the loss is borne by OMPxContract reserves and crystallized as profit for the adversary.
4.4 Why this is an ACT opportunity
All components of the exploit are permissionless:
- Balancer Vault flash-loan and WETH unwrap/rewrap flows are callable by any contract.
- OMPxContract’s
purchaseandbuyBackfunctions are public and non-owner-gated. - The helper contract is a standard adversary-deployed orchestrator without privileged roles.
Any unprivileged actor with access to the same on-chain contracts and sufficient technical understanding could reproduce the exploit by deploying an equivalent helper and sending the same type of transaction.
5. Adversary Flow Analysis
5.1 Adversary-related cluster
-
0x40d115198d71cab59668b51dd112a07d273d5831(EOA):- Originator of the exploit tx
0xd927843e30c6b2bf43103d83bca6abead648eac3cad0d05b1b0eb84cd87de9b6. - Deployed helper contract in the immediately preceding tx
0xfd1b40b640d0bb798499387a15a6adc1c97bed918edcdf7a58e38068aac14af2at block 20468778. - Receives the helper’s final
SELFDESTRUCTpayout.
- Originator of the exploit tx
-
0xfaddf57d079b01e53d1fe3476cc83e9bcc705854(helper/orchestrator contract):- Created by the EOA one block earlier.
- Implements Balancer/WETH interaction and the looping strategy against OMPxContract.
- Self-destructs to the EOA at the end of the exploit.
5.2 End-to-end sequence
-
Helper deployment (block 20468778, tx 0xfd1b40b6...4af2)
EOA0x40d1...5831deploys helper contract0xfadd...5854. The decompiled bytecode shows functions:// Helper contract (0xfadd...5854), Heimdall decompile excerpt function kill(address arg0) public { /* onlyOwner selfdestruct */ } function Unresolved_70bfc736(address arg0) public pure { /* argument check */ } function Unresolved_a04a0908(address arg0, uint256 arg1, uint256 arg2) public payable { /* transfer logic */ }Ownership and the presence of a
kill(address)onlyOwner function confirm that the EOA controls the helper. -
Flash-loan priming (block 20468780, tx 0xd927...de9b6, outer call)
The exploit tx is sent from the EOA to the helper with calldata selector0x70bfc736and arguments pointing to OMPxContract and OMPxToken. Inside the helper:- Balancer Vault transfers
100 WETHto the helper. - The helper calls WETH’s
withdraw(100 ETH)and receives100 ETH. - The helper sends
100 ETHto OMPxContract and begins the looping sequence.
- Balancer Vault transfers
-
Exploit execution loop against OMPxContract (same tx)
Within the same transaction, the helper executes multiple iterations of:purchase(uint256,uint256)via selector0x70876c98with value100 ETHand a very large token amount parameter.buyBack(uint256,uint256)via selector0x47d8167dwith the same large token amount.
Internal traces show that each iteration:
- Transfers
100 ETHfrom helper to OMPxContract. - Causes OMPxContract to transfer a slightly smaller amount of ETH back to the helper.
- Moves OMPxToken between OMPxContract and the helper without materially changing totalSupply.
Over dozens of iterations, the net effect is an ETH shortfall of
4.372869914943298279from OMPxContract, consistent with the prestate balance diff. -
Flash-loan repayment and profit realization (same tx)
After completing the loop:- The helper wraps enough ETH back into WETH via
WETH.deposit. - It repays the Balancer Vault
100 WETHplus protocol fees using standard ERC-20transfercalls. - The helper then executes
SELFDESTRUCT, sending its remaining ETH balance0x3caf8e04e9301ae7wei (~4.3714 ETH) to the EOA.
- The helper wraps enough ETH back into WETH via
-
Post-exploit consolidation
Subsequent transactions from the EOA (outside the seed tx) redistribute the profit, but no further interaction with OMPxContract is required to realize the gain. All exploit value transfer is completed within the single transaction0xd927...de9b6.
6. Impact & Losses
- Victim contract: OMPxContract
0x09a80172ed7335660327cd664876b5df6fe06108on Ethereum mainnet. - Primary loss:
- Native ETH loss from OMPxContract:
4.372869914943298279ETH (delta_wei-4372869914943298279).
- Native ETH loss from OMPxContract:
- Adversary profit:
- EOA
0x40d1...5831net gain:4.371401571200395847ETH (delta_wei+4371401571200395847). - Remaining ~0.0888 ETH is paid out to
0xdf99a0839818b3f120ebac9b73f82b617dc6a555as fees/miner payout.
- EOA
From a protocol perspective, the entire 4.372869914943298279 ETH loss is borne by OMPxContract’s reserves, directly reducing the ETH collateral backing OMPxToken and harming all token holders. The bonding-curve invariant that reserves should track outstanding supply at the advertised curve price is broken by the adversary’s loop.
7. References
-
Seed transaction and traces
- Ethereum mainnet tx
0xd927843e30c6b2bf43103d83bca6abead648eac3cad0d05b1b0eb84cd87de9b6: raw tx, receipt, and QuickNode callTracer trace under
artifacts/root_cause/data_collector/iter_2/tx/1/0xd927...de9b6/{tx.json,receipt.json,trace.call_tracer.json}. - Prestate native balance diff for the same tx under
artifacts/root_cause/data_collector/iter_2/tx/1/0xd927...de9b6/balance_diff.prestate_tracer.json.
- Ethereum mainnet tx
-
Contracts
- OMPxContract verified source (bonding-curve + pricing logic) at
artifacts/root_cause/data_collector/iter_1/contract/1/0x09a80172ed7335660327cd664876b5df6fe06108/source/etherscan_getsourcecode.json. - OMPxToken verified source at
artifacts/root_cause/data_collector/iter_1/contract/1/0x633b041c41f61d04089880d7b5c7ed0f10ff6f85/source/etherscan_getsourcecode.json. - Helper contract
0xfaddf57d079b01e53d1fe3476cc83e9bcc705854Heimdall decompiled code at
artifacts/root_cause/data_collector/iter_2/contract/1/0xfaddf57d079b01e53d1fe3476cc83e9bcc705854/decompile/0xfaddf57d079b01e53d1fe3476cc83e9bcc705854-decompiled.sol.
- OMPxContract verified source (bonding-curve + pricing logic) at
-
Account histories
- EOA
0x40d115198d71cab59668b51dd112a07d273d5831normal txlist (showing helper deployment and exploit tx) at
artifacts/root_cause/data_collector/iter_2/address/1/0x40d115198d71cab59668b51dd112a07d273d5831/txlist.normal.etherscan_v2.json. - OMPxContract internal txlist around the incident block window at
artifacts/root_cause/data_collector/iter_2/address/1/0x09a80172ed7335660327cd664876b5df6fe06108/txlist.internal.etherscan_v2.json.
- EOA