We do not have a reliable USD price for the recorded assets yet.
0xae7d664bdfcc54220df4f18d339005c6faf6e62c9ca79c56387bc0389274363b0x0eEe3E3828A45f7601D5F54bF49bB01d1A9dF5eaEthereum0x3212b29e33587a00fb1c83346f5dbfa69a458923EthereumAn unprivileged adversary exploited dForce Lendf.Me MoneyMarket on Ethereum by combining MoneyMarket's delayed supply-accounting updates with IMBTC's ERC777 sender hook. In exploit transaction 0xae7d664bdfcc54220df4f18d339005c6faf6e62c9ca79c56387bc0389274363b at block 9899738, attacker helper contract 0x538359785a8d5ab1a741a0ba94f26a800759d91d reentered MoneyMarket.withdraw(IMBTC, 43188) from inside IMBTC.transferFrom(...) while an outer MoneyMarket.supply(IMBTC, 1) call was still in progress. The reentrant withdraw reclaimed IMBTC against stale supply state, and when the outer supply resumed it persisted a fresh positive supply balance anyway. The result was a one-transaction increase of 43188 raw IMBTC units in the adversary's combined wallet-plus-supply position.
The root cause is a checks-effects-interactions failure across two public contracts. MoneyMarket externalizes control before persisting updated supply balances, and IMBTC's ERC777 implementation invokes tokensToSend before moving balances. That composition lets an attacker-controlled sender contract observe stale supply principal, withdraw against it, and still complete the outer supply path.
dForce Lendf.Me's MoneyMarket contract at 0x0eEe3E3828A45f7601D5F54bF49bB01d1A9dF5ea tracks each supplier's principal in supplyBalances[account][asset] and exposes public supply(address,uint256) and entrypoints. Each action first computes current balances and updated totals, then later writes the resulting principal and market indexes back to storage.
withdraw(address,uint256)The asset used here is IMBTC at 0x3212b29e33587a00fb1c83346f5dbfa69a458923. IMBTC is not a plain ERC20 in execution terms: its transferFrom path invokes the ERC777 sender hook for the token holder if a sender implementer is registered in ERC1820. That means a token transfer initiated by MoneyMarket can synchronously call arbitrary attacker code before the token balances are actually moved.
The attacker controlled EOA 0xa9bf70a420d364e923c74448d9d817d3f2a77822 and helper contract 0x538359785a8d5ab1a741a0ba94f26a800759d91d. The helper had already been funded with IMBTC and could register itself as its own ERC777 sender implementer, which made it the execution point for the reentrant callback during supply().
The vulnerability is a reentrancy-enabled accounting duplication in supply-side bookkeeping. MoneyMarket's supply() function computes the user's updated supply balance before calling doTransferIn, but it does not persist that balance until after the external token transfer returns. withdraw() has the same structural problem in reverse: it computes the reduced post-withdraw balance, transfers tokens out with doTransferOut, and only then writes the reduced principal. IMBTC's ERC777 transferFrom() implementation expands the attack surface because it calls _callTokensToSend(...) before _move(...), so the sender contract gains synchronous control while MoneyMarket still believes the old principal is intact.
The invariant violated by the exploit is straightforward: a supplier's claimable MoneyMarket balance plus its in-wallet IMBTC must not increase unless the protocol receives a matching net IMBTC inflow and records that inflow exactly once. In the exploit path, the attacker first establishes a positive supply principal, then starts a second supply() with amount 1. During the nested ERC777 callback, the helper reenters withdraw() and pulls out IMBTC against the stale principal. When the outer supply() resumes, only 1 IMBTC is actually transferred in, but the contract still writes a fresh positive principal as if the prior position had never been withdrawn.
The verified MoneyMarket source shows the critical ordering in supply():
function supply(address asset, uint amount) public returns (uint) {
...
(err, localResults.userSupplyUpdated) = add(localResults.userSupplyCurrent, amount);
...
err = doTransferIn(asset, msg.sender, amount);
...
market.totalSupply = localResults.newTotalSupply;
...
balance.principal = localResults.userSupplyUpdated;
}
The verified withdraw() source has the same issue:
function withdraw(address asset, uint requestedAmount) public returns (uint) {
...
(err, localResults.userSupplyUpdated) = sub(localResults.userSupplyCurrent, localResults.withdrawAmount);
...
err = doTransferOut(asset, msg.sender, localResults.withdrawAmount);
...
supplyBalance.principal = localResults.userSupplyUpdated;
}
The collected IMBTC source confirms why doTransferIn() is reentrancy-relevant:
function _transferFrom(address holder, address recipient, uint256 amount) internal returns (bool) {
address spender = msg.sender;
_callTokensToSend(spender, holder, recipient, amount, "", "");
_move(spender, holder, recipient, amount, "", "");
_approve(holder, spender, _allowances[holder][spender].sub(amount));
}
The seed trace demonstrates the exploit sequence on-chain. First, the helper performs a large supply and receives a positive IMBTC supply balance. Later in the same transaction it calls MoneyMarket::supply(IMBTC, 1). Inside that call, IMBTC::transferFrom(...) invokes tokensToSend(...) on the helper, and the helper immediately reenters MoneyMarket::withdraw(IMBTC, 43188). The trace then shows emit SupplyWithdrawn(..., 43188, 43188, 0), which proves the withdraw path consumed the stale pre-callback principal and reduced recorded balance to zero at that moment. After control returns to the outer token transfer, the same trace shows emit SupplyReceived(..., 1, 0, 43189), proving the original supply() call persisted a fresh positive principal after the withdraw already returned IMBTC to the attacker. That is the concrete accounting duplication breakpoint.
The exploit sequence starts with helper deployment in transaction 0xb7449a3a2a36bb3997162fc02759dc4924f098703a4cc7977dbfc540d3ed1168, where the attacker EOA created helper contract 0x538359785a8d5ab1a741a0ba94f26a800759d91d. The attacker then seeded the helper with IMBTC in transaction 0x88fa4e8609baac44189a58faf7cb740cf35308957832ffd6656999229fea689f, transferring 21595 raw IMBTC units into the helper.
In exploit transaction 0xae7d664bdfcc54220df4f18d339005c6faf6e62c9ca79c56387bc0389274363b, the helper approves MoneyMarket and registers itself in ERC1820 as an ERC777TokensSender implementer. It then executes one large supply(IMBTC, 21593) to create a substantial supply principal and follows with a second supply(IMBTC, 1) to trigger the vulnerable callback window. During the second supply's transferFrom, ERC1820 resolves the helper as the sender implementer and dispatches tokensToSend(...) to the attacker helper.
At that decision point, the helper reads its current IMBTC supply balance and calls withdraw(IMBTC, 43188) before the outer supply call can store updated balances. The trace shows the withdraw succeeds, IMBTC is transferred back out of MoneyMarket to the helper, and SupplyWithdrawn records a zero new balance. Once the callback returns, IMBTC transfers the final 1 unit into MoneyMarket, and the outer supply() persists a renewed principal of 43189. The adversary therefore exits the transaction with both wallet IMBTC and a surviving MoneyMarket supply claim, which is exactly the exploit predicate that the PoC later reproduces on a fresh fork.
This exploit iteration increased the adversary's IMBTC-denominated position by 43188 raw units, with IMBTC using 8 decimals. The attacker did not need privileged keys, governance access, or any private protocol hook; the sequence depended only on public contract entrypoints, ERC1820 registration, and the protocol's vulnerable accounting order.
The measurable loss is the protocol-side accounting shortfall created when withdrawn IMBTC left MoneyMarket but the supplier principal was still restored by the outer supply() path. In concrete terms, after the transaction the attacker helper held 43188 raw IMBTC units in-wallet and also retained a MoneyMarket IMBTC supply balance of 43189, even though the second supply transferred in only 1 raw unit during the reentrant cycle.
0xae7d664bdfcc54220df4f18d339005c6faf6e62c9ca79c56387bc0389274363b0xb7449a3a2a36bb3997162fc02759dc4924f098703a4cc7977dbfc540d3ed11680x88fa4e8609baac44189a58faf7cb740cf35308957832ffd6656999229fea689fMoneyMarket at 0x0eEe3E3828A45f7601D5F54bF49bB01d1A9dF5eaIMBTC at 0x3212b29e33587a00fb1c83346f5dbfa69a4589230xae7d...