
7 min read
Fake ERC-20 Deposit Exploits: How Forged Event Logs Trick Crypto Indexers
On 24 July 2026, a contract on Ethereum emitted a log that said 0.29 ETH had arrived at an address. No 0.29 ETH arrived anywhere. The transaction succeeded, the block confirmed, and the record on chain now says a transfer happened.
That gap — between what the chain records and what the chain did — is the entire attack. It is not a hack of an exchange. It is a forged receipt, submitted to a system that was built to trust receipts.
The class is old. Researchers documented the fake deposit vulnerability in a 2021 IEEE paper, and CoinDesk reported roughly $1bn of exposed tokens in 2020. What is worth your time is not that it exists. It is how cheap the 2026 version is, and how ordinary it looks in a monitoring dashboard.
What the transaction actually contains
An externally owned account calls a contract. The contract produces two internal transactions and writes one event log. Total value at risk to the attacker: gas.
The event log is the payload. It reads:
Transfer (index_topic_1 address to, uint256 amount)
to: 0xC5518694Ef1A64eF0362eB5165A8A41e83Ff4C41
amount: 290000000000000000
Read the signature, not the name.
The standard ERC-20 transfer event is Transfer(address,address,uint256) — a sender, a recipient, an amount, with the first two indexed. Three topics in the log.
This event has one indexed parameter and two topics. There is no from. That is the whole trick.
A real transfer records who sent it. This one only records who received it, because nothing was sent. The contract declared its own event, named it Transfer, and gave it a shape that no token standard produces. Its topic hash — 0x69ca02dd4edd7bf0a4abb9ed3b7af3f14778db5d61921c... — is not the ERC-20 signature hash. It is a different event wearing the same name.
The internal transactions supply the corroboration. Both are labelled Transfer, both carry 0.29 ETH, both originate from the contract. One points at the address named in the counterfeit log. The other points back at the account that started the whole thing. One of them executes via callcode, a deprecated opcode that runs borrowed code in the caller's own context and is close to extinct in legitimate modern contracts.
Anyone eyeballing the trace sees a contract that received a call and moved ETH. Anything parsing it sees a deposit.
Why an exchange credits it
Deposit crediting is an indexing problem, and indexers are pattern matchers. A deposit pipeline watches the chain, decides which events represent value arriving at an address it controls, and posts a balance. The decision is usually made on some combination of: does an event named Transfer exist, does the to field match a deposit address, is the amount parseable, did the transaction succeed.
Every one of those is true here. The transaction succeeded. The event is named Transfer. The recipient matches. The amount parses cleanly to 0.29 ETH.
The check that fails is one many pipelines never make: does the event's topic hash match the ERC-20 standard signature, and does the resulting balance change actually exist in state?
Match on the event's name and you can be fed anything. Match on the signature hash and verify against balance, and this transaction is inert.
Then the second half runs. Once the balance posts, the attacker withdraws immediately, against a credit backed by nothing. The exchange sends real assets. The counterfeit deposit is never reconciled because most reconciliation happens in batch, hours later, and the withdrawal has already settled.
Speed is not incidental. It is the product.
What makes the 2026 version worth flagging
Three things, none of which are novel individually.
- The cost is gas. No capital is committed and nothing is at risk if the attempt fails, which means an operator can run it against a hundred venues and only needs one indexer to be permissive.
- The amount is small. 0.29 ETH does not trip a large-transfer alert. The purpose of the first attempt is not profit — it is finding out whether your pipeline credits it. What follows is sized to what your withdrawal limits allow.
- It is legible on chain. Every element is public and confirmed. There is no exploit, no stolen key, no compromised endpoint. Your logs will show a normal day.
That last point is the one that matters operationally. Attacks that leave no trace are hard to detect. This one leaves a perfect trace and is still hard to detect, because the trace looks correct.
Three checks
- Match the signature hash, not the event name. Your indexer should compare topic0 against the canonical ERC-20 Transfer hash and discard anything else, however it is labelled. This is the single change that stops the attack described here.
- Verify against state, not against events. An event is a claim. Query the balance and confirm it moved before crediting. Events are a convenience for indexing; they were never designed as proof of settlement, and treating them as proof is the underlying error.
- Put time between crediting and withdrawal. Whatever your confirmation policy, a first withdrawal that consumes a deposit credited seconds earlier deserves a different path than a routine one. The attack depends on the two events being adjacent. Separating them costs you very little and costs the attacker the entire scheme.
For operators running these controls in production rather than designing them from scratch: withdrawal authorization is a policy layer, not a feature. It belongs where approval flows, thresholds and escalation already live — which is also where an unforgeable record of who authorized what gets written.
The thing to take away
Nobody broke into anything on 24 July. A contract wrote a sentence about itself, and the sentence was designed to be misread by software that reads quickly. Your deposit pipeline is not a ledger reader. It is a document parser, and it has been quietly trusting the document to be honest about its own contents.

