Loan Foreclosure Drains Pooled ETH Via Global Balance Distribution
Exploit Transactions
0x2c728af49d254dd60b0e0918106f32873b5c8492f9b6f3964227373a8e6e9fab0x5ae94fb2225f473ecb302bf35170cac826beedc2766bcd0ab9d35707df07f9d30xc1c4f4c11cd3ef56c77f40906f5c3b061c60688b2cdea6b141ca68f3c9c055530x26eb9f4e7c8ab5eb589dfc7f447486cf8e557d91646d51927d86b8969da98090Victim Addresses
0xdb005b73f591922b4689824aa4035053269ffa44EthereumLoss Breakdown
Similar Incidents
NFTX Doodles collateral accounting flaw enables flash-loan ETH extraction
32%AnyswapV4Router WETH9 permit misuse drains WETH to ETH
32%OMPxContract bonding-curve loop exploit drains ETH reserves
31%bZx/Fulcrum WBTC market manipulation drains ETH liquidity
31%Access-control bug draining 5 ETH from token contract
30%Beanstalk flash-loan governance takeover drains treasury assets
30%Root Cause Analysis
Loan Foreclosure Drains Pooled ETH Via Global Balance Distribution
1. Incident Overview TL;DR
An unprivileged adversary drained ETH from the Loan Contract proxy at 0xdb005b73f591922b4689824aa4035053269ffa44 by creating a new loan (loanId=14), assigning themselves as the only shareholders for that loan, and then triggering foreclosure. The foreclosure payout logic computed the distribution amount using a function that returns the proxy's global ETH balance rather than a per-loan balance, so foreclosing the attacker-controlled loan distributed pooled ETH that belonged to other loans/protocol state.
Root cause: initiateLoanForeclose(uint256 loanId) uses getContractBalance(loanId) as the payout base, but getContractBalance(uint256) is effectively independent of loanId and matches address(proxy).balance, breaking per-loan asset isolation.
2. Key Background
- Victim contract: upgradeable proxy at
0xdb005b73f591922b4689824aa4035053269ffa44(delegatecalls into implementation0x03f44e563dd447449f48f8103b5df70aff7cf577, visible in the seed trace). - Loans are tracked by an integer
loanIdand have a shareholder/share mechanism. - On-chain state about a loan's shareholders is queryable via view functions (used by the auditor/collector evidence):
getSHAddress(uint256 loanId, uint256 idx)to enumerate shareholder addresses.getShareholderShares(uint256 loanId, address shareholder)to read each shareholder's shares for a given loan.
- Foreclosure: a function
initiateLoanForeclose(uint256 loanId)distributes ETH to the loan's shareholders. - Critical accounting function:
getContractBalance(uint256 loanId)is expected (by name/usage) to represent a per-loan balance, but empirically returns the proxy's global ETH balance.
3. Vulnerability Analysis & Root Cause Summary
This is an accounting / asset-isolation bug in the foreclosure payout calculation. The protocol stores multiple loans' ETH inside a single contract balance (pooled custody), and foreclosure should distribute only the ETH attributable to the specified loanId. Instead, the distribution amount is derived from a function that returns the contract's total ETH balance regardless of loanId. As a result, an attacker can create a fresh loan with attacker-controlled shareholders, then foreclose it to withdraw pooled ETH that is unrelated to the attacker-created loan. The exploit does not require privileged roles or compromised keys; it uses publicly callable entrypoints and on-chain state. The seed transaction shows the proxy paying out nearly all pooled ETH to the attacker-controlled shareholders, after which those shareholders forward ETH to the attacker EOA.
4. Detailed Root Cause Analysis
4.1 Pre-state facts (block 24441150)
At the end of block 24441150 (pre-state for the exploit tx), the victim proxy held a large pooled ETH balance:
contract ETH balance (block 24441150): 5302821111758431738 wei (5.302821111758431738 ETH)
The key behavioral indicator for the bug is that getContractBalance(loanId) returns the same value for multiple different loanIds at this pre-state:
getContractBalance(<loanId>) at block 24441150:
loan 0: 5302821111758431738
loan 1: 5302821111758431738
loan 2: 5302821111758431738
loan 13: 5302821111758431738
loan 14: 5302821111758431738
This demonstrates that the function is independent of loanId and is tied to the proxy's global ETH balance.
Separately, loan 14 was created shortly before the exploit and had only attacker-controlled shareholders at the pre-state block:
Loan 14 shareholders at block 24441150:
0x76788300996D41a03676bBab19A71D616f782BB7 (100 shares)
0x608d18C6B73ef7af28F9fE6E7a72694957458699 (10395 shares)
0x656Eb28d9AbD6bAEA34eC1bb31E636626Df2c4de (10395 shares)
Total shares: 20890
4.2 Exploit trigger (block 24441151, tx 0x26eb…8090)
In block 24441151, the exploit is realized in transaction:
0x26eb9f4e7c8ab5eb589dfc7f447486cf8e557d91646d51927d86b8969da98090
Observed effects from canonical balance diffs for this transaction:
{
"victim_proxy": {
"address": "0xdb005b73f591922b4689824aa4035053269ffa44",
"before_wei": "5302821111758431738",
"after_wei": "50373127912881386",
"delta_wei": "-5252447983845550352"
}
}
So the proxy loses 5252447983845550352 wei (5.252447983845550352 ETH) during the exploit transaction.
The seed trace shows the proxy transferring ETH to the three loan-14 shareholder addresses (amounts match the share proportions):
0x767883... (100 / 20890 shares) receives 25143360382219006 wei (0.025143360382219006 ETH)
0x608d18... (10395 / 20890 shares) receives 2613652311731665673 wei (2.613652311731665673 ETH)
0x656eb2... (10395 / 20890 shares) receives 2613652311731665673 wei (2.613652311731665673 ETH)
Total transferred: 5252447983845550352 wei (5.252447983845550352 ETH)
Because those recipients are the attacker-controlled shareholders for the newly created loan 14, and because the payout base is the contract's global ETH balance rather than a loan-14-specific balance, the attacker can withdraw pooled ETH that was already in the contract prior to creating loan 14.
4.3 Profit realization
After receiving ETH from the victim proxy, the shareholder contracts forward ETH to the attacker EOA (0x3b1e24061478560d91f72f895e0cf7972f45d1ef) within the same transaction (visible in the trace as fallback{value: ...} calls).
The collector's native balance delta for the attacker EOA over the exploit transaction is:
attacker EOA before: 0.032975056195356989 ETH
attacker EOA after: 5.289685302397209232 ETH
delta: +5.256710246201852243 ETH (net of gas)
5. Adversary Flow Analysis
The incident's on-chain sequence (all permissionless transactions from an unprivileged EOA) is:
0x2c728af49d254dd60b0e0918106f32873b5c8492f9b6f3964227373a8e6e9fab(block 24441146): deploy helper contract0x608d18c6b73ef7af28f9fe6e7a72694957458699.0x5ae94fb2225f473ecb302bf35170cac826beedc2766bcd0ab9d35707df07f9d3(block 24441148): transfer50 DAIto the helper contract.0xc1c4f4c11cd3ef56c77f40906f5c3b061c60688b2cdea6b141ca68f3c9c05553(block 24441149): create loan14, deposit0.050538154368260197 ETHas collateral, and set loan-14 shareholders to attacker-controlled contract addresses.0x26eb9f4e7c8ab5eb589dfc7f447486cf8e557d91646d51927d86b8969da98090(block 24441151): triggerinitiateLoanForeclose(14)(via the helper stack), causing the victim proxy to distribute pooled ETH to loan-14 shareholders, which then forward ETH to the attacker EOA.
Key decision point: once the attacker has a loan with attacker-only shareholders, triggering foreclosure causes the protocol to compute payouts from the global balance, not from loan-specific collateral, enabling the drain.
6. Impact & Losses
- Victim:
0xdb005b73f591922b4689824aa4035053269ffa44 - Asset: ETH
- Loss (victim proxy balance reduction in the exploit tx):
5.252447983845550352 ETH(5252447983845550352 wei) - Attacker profit (EOA net balance delta, after gas):
5.256710246201852243 ETH(profit exceeds victim delta due to intra-tx forwarding and accounting of intermediate contract balances; victim delta is the canonical protocol loss)
7. References
- Exploit transaction (seed):
0x26eb9f4e7c8ab5eb589dfc7f447486cf8e557d91646d51927d86b8969da98090 - Victim proxy:
0xdb005b73f591922b4689824aa4035053269ffa44 - Proxy implementation (as observed via delegatecall in trace):
0x03f44e563dd447449f48f8103b5df70aff7cf577 - Trace evidence (cast run):
artifacts/collector/seed/1/0x26eb9f4e7c8ab5eb589dfc7f447486cf8e557d91646d51927d86b8969da98090/trace.cast.log - Balance delta evidence:
artifacts/collector/seed/1/0x26eb9f4e7c8ab5eb589dfc7f447486cf8e557d91646d51927d86b8969da98090/balance_diff.json - Pre-state calls demonstrating
getContractBalanceis independent ofloanId:artifacts/auditor/iter_0/onchain/contract_balance_calls.txt - Loan-14 shareholder state at pre-state:
artifacts/auditor/iter_0/onchain/loan14_shareholders_block24441150.txt - Loan-14 lifecycle and victim balance across blocks:
artifacts/auditor/iter_0/onchain/loan14_state_across_blocks.txt