Track 7 · The Breach Lab · lesson 4
tx.origin phishing
15 min
Track 7 · The Breach Lab · lesson 4
15 min
Track 2 said tx.origin is almost always the wrong choice. Here is the attack
that makes it true.
The wallet below authorises transfers by checking tx.origin == owner. The
owner has just been tricked — by a fake airdrop, a "claim your NFT" button,
anything — into calling attack() on your contract.
That is all you need.
PhishableWallet
? ETH
tx.origin is the account that started the transaction. msg.sender is the
immediate caller. When the owner calls your contract, and your contract calls
the wallet:
msg.sender is your contracttx.origin is still the ownerThe wallet asked the wrong question. It wanted to know "did the owner authorise this action?" and instead asked "did the owner start this transaction?" — which is true even when the owner has no idea what the transaction does.
msg.sender cannot be borrowed this way. Had the wallet checked msg.sender == owner, it would have seen your contract, not the owner, and reverted. The fix
is a single word.
The technical flaw is one line, but the attack needs the owner to call your contract. That is the phishing part: a convincing reason to click.
This is why "just don't click suspicious links" is weak advice. The link only
has to reach one function call, and a tx.origin wallet does the rest. The real
defence is contracts that cannot be abused even when the owner is fooled.
Predict
There is a narrow, defensible use: require(tx.origin == msg.sender) to check
that the caller is an externally owned account and not a contract.
It is used to block flash-loan-powered attacks that must run inside a single transaction. It is also increasingly discouraged, because it breaks smart-contract wallets — which are exactly the accounts you often want to support. Account abstraction is making it obsolete.
So: tx.origin for authorisation, never. tx.origin for EOA detection, rarely
and with awareness of the cost.
Check