Calculated from recorded token losses using historical USD prices at the incident time.
0x7d2296bcb936aa5e2397ddf8ccba59f54a178c3901666b49291d880369dbcf310xb8919522331c59f5c16bdfaa6e03a91f62EthereumOn Ethereum mainnet block 14326932, adversary EOA 0x7c42f2a7d9ad01294ecef9be1e38272c84607593 used its helper contract 0x580cac65c2620d194371ef29eb887a7d8dcc91bf to execute a flash‑swap‑backed sequence of three lend(uint256) calls followed by one redeem(uint256) call against the Pool16 proxy 0xb8919522331c59f5c16bdfaa6e03a91f62. This sequence extracted exactly 976,924.997605 USDC from the pool. After repaying the UniswapV2 USDC/ETH flash swap, the adversary cluster retained 957,208.997605 USDC as profit, while HOME totalSupply and all HOME balances on Pool16 remained zero before and after the attack.
The root cause is that the deployed Pool16 implementation at 0x781ad73f140815763d9a4d4752daf9203361d07d prices HOME redemptions purely from two internal bookkeeping variables poolLent and poolBorrowed and an attacker‑controlled amountHome parameter, without enforcing any linkage to HOME totalSupply or per‑address HOME balances. As a result, an unprivileged caller can redeem USDC from Pool16 even when no HOME has ever been minted and no HOME balances exist.
Pool16 (HomeCoin) is an upgradeable ERC20‑based lending pool where users are intended to deposit USDC and receive HOME tokens that represent their share of the pool. The verified source shows that the contract tracks both the ERC20 HOME supply and balances, and a set of legacy fields:
Pool16.solpoolLent and poolBorrowed are marked as deprecated in Pool14 and are documented as “always set to 0” in the upgraded architecture.totalSupply() and an internal _balances mapping, with _balances stored starting at slot 0x33.In the live system, the Pool16 proxy at 0xb8919522331c59f5c16bdfaa6e03a91f62 delegates calls to an implementation contract at 0x781ad73f140815763d9a4d4752daf9203361d07d. Storage snapshots around the exploit show:
14326931 (0xda9c93), the Pool16 proxy holds 978,497.348951 USDC.totalSupply at slot 0x52 is 0._accrued at slot 0x60 is 0.poolLent at slot 0x68 is 6,667,864,245,841.poolBorrowed at slot 0x69 is 5,689,859,839,693.loanCount at slots 0x6b and 0x6c is 28._balances mapping entries for the helper 0x580cac65c2620d194371ef29eb887a7d8dcc91bf and the adversary EOA 0x7c42f2a7d9ad01294ecef9be1e38272c84607593 are both zero.These values are taken directly from the pre/post storage dump:
{
"slots": {
"0x52": {
"pre": "0x0",
"post": "0x0"
},
"0x60": {
"pre": "0x0",
"post": "0x0"
},
"0x68": {
"pre": "0x000000000000000000000000000000000000000000000000000006107bae0651",
"post": "0x0000000000000000000000000000000000000000000000000000052d06698e6c"
},
"0x69": {
"pre": "0x0000000000000000000000000000000000000000000000000000052cc61316cd",
"post": "0x0000000000000000000000000000000000000000000000000000052cc61316cd"
}
}
}
Caption: Pool16 proxy storage pre/post exploit (block 14326931→14326932) for key slots, showing totalSupply and _accrued fixed at 0 while poolLent decreases.
The USDC token is FiatTokenV2_2 at 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, and the exploit uses the canonical UniswapV2 USDC/ETH pair 0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc to obtain a flash swap.
The vulnerability is an accounting invariant violation between HOME shares and underlying USDC assets. In the intended design, only holders of minted HOME should be able to redeem USDC, and the pool’s USDC balance should remain consistent with HOME totalSupply and individual HOME balances. However, in the deployed implementation:
lend(uint256) and redeem(uint256) are implemented to read and write only the legacy poolLent and poolBorrowed storage slots and to transfer USDC, without touching HOME totalSupply or the _balances mapping.amountHome (a caller‑controlled parameter) and the internal poolLent/poolBorrowed values, with no check that the caller owns any HOME or that totalSupply is non‑zero.In the observed state, HOME totalSupply and all HOME balances are zero while Pool16 holds a large USDC balance. Under these conditions, an unprivileged caller can choose an amountHome input that drives redeem(uint256) to transfer out nearly all USDC from the pool, even though no HOME has ever been minted. This is the precise root cause exploited by the adversary.
Let:
A be Pool16’s USDC balance.S be HOME totalSupply (slot 0x52).L be poolLent (slot 0x68).B be poolBorrowed (slot 0x69).bal[x] be the HOME balance of address x (_balances mapping at slot 0x33).For any redemption of amountHome from address x, the intended invariant is:
amountHome <= bal[x],S > 0, andx must be limited so that A, L, B, and S remain mutually consistent, with no USDC redeemable when S = 0 and all bal[*] = 0.The verified Pool16.sol source marks poolLent and poolBorrowed as deprecated and intended to remain at zero in the upgraded design. The deployed implementation at 0x781ad73f140815763d9a4d4752daf9203361d07d, however, repurposes these slots in the actual lending and redemption logic:
lend(uint256 amountUsdc) that:
0x66.transferFrom(msg.sender, address(this), amountUsdc) on FiatTokenV2_2.poolLent from slot 0x68, performs arithmetic combining amountUsdc with the prior poolLent value, and writes a new value back to slot 0x68 with SSTORE.redeem(uint256 amountHome):
poolLent (slot 0x68) and poolBorrowed (slot 0x69) into local variables.amountHome, L, and B to compute a USDC payout and an updated L'.L' back to slot 0x68.transfer to send USDC from Pool16 to msg.sender.Across this routine, there are **no SLOAD or SSTORE operations touching HOME totalSupply at slot 0x52 or any _balances entries at slot 0x33. The contract never checks that msg.sender owns HOME nor that totalSupply is non‑zero before redeeming USDC.
From the pre/post storage snapshot for the Pool16 proxy:
S_pre = 0, _accrued_pre = 0, L_pre = 6,667,864,245,841, B_pre = 5,689,859,839,693, loanCount_pre = 28.S_post = 0, _accrued_post = 0, L_post = 5,690,939,248,236, B_post = 5,689,859,839,693, loanCount_post = 28._balances entries for both the helper and the adversary EOA remain zero before and after.Computing the change in poolLent:
python - << 'PY'
pre = int('6107bae0651', 16)
post = int('52d06698e6c', 16)
print(post - pre)
PY
This yields -976,924,997,605, which equals the Pool16 proxy’s USDC balance delta in the FiatTokenV2_2 balance diff for the exploit transaction:
{
"token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"holder": "0xb8919522331c59f5c16bdfaa6e03a91f62",
"before": "978497348951",
"after": "1572351346",
"delta": "-976924997605"
}
Caption: FiatTokenV2_2 USDC balance diff for Pool16 proxy in tx 0x7d2296bcb936aa5e2397ddf8ccba59f54a178c3901666b49291d880369dbcf31, matching the poolLent decrease.
Thus, in the exploit transaction:
delta A = -976,924,997,605 (USDC leaves Pool16),delta L = -976,924,997,605,S_pre = S_post = 0,The call trace for tx 0x7d2296bcb936aa5e2397ddf8ccba59f54a178c3901666b49291d880369dbcf31 shows:
… 0x781ad73f140815763d9A4D4752DAf9203361D07D::lend(2120000000000) [delegatecall]
… 0x781ad73f140815763d9A4D4752DAf9203361D07D::lend(2120000000000) [delegatecall]
… 0x781ad73f140815763d9A4D4752DAf9203361D07D::lend(2120000000000) [delegatecall]
… 0x781ad73f140815763d9A4D4752DAf9203361D07D::redeem(8465943180104) [delegatecall]
Caption: Extract from trace.cast.log for tx 0x7d2296…dbcf31, showing three lend calls and one redeem call via the Pool16 implementation.
These three lends deposit USDC into Pool16 and update poolLent, and the final redeem chooses amountHome = 8,465,943,180,104 so that the internal formula reduces poolLent by 976,924,997,605 and transfers exactly that amount of USDC to the helper, all while HOME totalSupply and HOME balances stay at zero. This is a direct violation of the intended invariant that only minted HOME should be redeemable for USDC and that ERC20 accounting should remain consistent with the pool’s assets.
The adversary‑related cluster consists of:
0x7c42f2a7d9ad01294ecef9be1e38272c84607593
balance_diff.json.0x580cac65c2620d194371ef29eb887a7d8dcc91bf
msg.sender for all lend(uint256) and redeem(uint256) delegatecalls to the Pool16 implementation in the exploit transaction.0xacfcaa8e1c482148f9f2d592c78ca7a27934c7333dab31978ed0aef333a28ab6 (block 14326928).0x7c42f2a7d9ad01294ecef9be1e38272c84607593.0x580cac65c2620d194371ef29eb887a7d8dcc91bf).0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48,0xb8919522331c59f5c16bdfaa6e03a91f62,0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc.0x7d2296bcb936aa5e2397ddf8ccba59f54a178c3901666b49291d880369dbcf31 (block 14326932).0x7c42f2a7d9ad01294ecef9be1e38272c84607593.0x580cac65c2620d194371ef29eb887a7d8dcc91bf with calldata 0xdfcbd40f…0005c8cd8a7000.Execution flow (from trace.cast.log and balance diffs):
0xb4e16…c9dc, borrowing USDC.0x781ad73f140815763d9a4d4752daf9203361d07d, the helper calls lend(uint256) three times with amountUsdc = 2,120,000,000,000 each, depositing USDC from the helper into Pool16 and incrementing poolLent.redeem(uint256) once with amountHome = 8,465,943,180,104, again via delegatecall into the Pool16 implementation.redeem(uint256) computes a USDC payout based solely on amountHome, poolLent, and poolBorrowed, writes the updated poolLent back to slot 0x68, and transfers 976,924.997605 USDC from Pool16 to the helper.957,208.997605 USDC net.The FiatTokenV2_2 balance_diff.json for this transaction summarizes the final ERC20 balances:
0xb891…f62): delta = -976,924.997605 USDC.0xb4e16…c9dc): delta = +19,716.000000 USDC.0x7c42…7593): delta = +957,208.997605 USDC.No HOME tokens are minted or transferred at any point; HOME totalSupply and all HOME balances remain zero, meaning the adversary’s profit is entirely due to the broken lend/redeem accounting.
The measurable impact on Pool16 in the exploit transaction 0x7d2296bcb936aa5e2397ddf8ccba59f54a178c3901666b49291d880369dbcf31 is:
976,924.997605 USDC.957,208.997605 USDC.19,716.000000 USDC (the flash swap fee/price impact).totalSupply and all HOME balances on Pool16 remain exactly zero before and after, so the pool’s recorded HOME liabilities do not change.From the protocol’s perspective, nearly one million USDC of assets are removed from Pool16 with no corresponding reduction in recorded HOME obligations. This leaves the pool severely under‑collateralized relative to its intended design and represents a direct economic loss to the protocol and its users.
0xb8919522331c59f5c16bdfaa6e03a91f62 on Ethereum mainnet.0x781ad73f140815763d9a4d4752daf9203361d07d.0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc.0x7c42f2a7d9ad01294ecef9be1e38272c84607593.0x580cac65c2620d194371ef29eb887a7d8dcc91bf.0x7d2296bcb936aa5e2397ddf8ccba59f54a178c3901666b49291d880369dbcf31 (block 14326932).0xacfcaa8e1c482148f9f2d592c78ca7a27934c7333dab31978ed0aef333a28ab6 (block 14326928).artifacts/root_cause/seed/1/0x366049d336e73cfaf39c6a933780ca4c96ea084c/src/PoolCore/Pool16.sol.artifacts/root_cause/data_collector/iter_3/contract/1/0x781ad73f140815763d9a4d4752daf9203361d07d/.artifacts/root_cause/data_collector/iter_4/storage/1/0xb8919522331c59f5c16bdfaa6e03a91f62/.artifacts/root_cause/seed/1/0x7d2296bcb936aa5e2397ddf8ccba59f54a178c3901666b49291d880369dbcf31/.artifacts/root_cause/seed/1/0xacfcaa8e1c482148f9f2d592c78ca7a27934c7333dab31978ed0aef333a28ab6/.