Who Really Called: The tx.origin Authentication Flaw
The challenge
Pembrook's on-chain vault lets only its owner withdraw, and the owner's key has never leaked. Yet the vault was drained after the owner used a new airdrop site that asked them to click Claim. The withdraw guard is doing what the author wrote; the author just asked the wrong question about who is calling. Read the contract and name the expression in the owner check that let a contract the owner merely interacted with spend on their behalf.
What you'll learn
- Distinguish msg.sender (immediate caller) from tx.origin (transaction signer)
- Explain how a phishing contract keeps tx.origin equal to the victim
- Identify tx.origin in an access check as the vulnerability
- Contrast it with a correct msg.sender ownership check
- State the rule: authorize with msg.sender, not tx.origin
Skills tested
Prerequisites
- Reading Solidity
- What a contract call chain is
How it works
Every Solidity call sees two identities. msg.sender is whoever called this function directly, which may be another contract. tx.origin is the externally owned account that signed the whole transaction, unchanged no matter how many contracts the call passes through.
Authorising on tx.origin is phishable. If the owner is lured into calling a malicious contract, and that contract calls the vault, then inside the vault msg.sender is the malicious contract but tx.origin is still the owner. A check of tx.origin == owner passes, and the attacker's code spends as the owner without ever touching the owner's key.
The same contract's transferOwnership shows the correct form, comparing msg.sender, which cannot be driven through an intermediary.
Common mistakes
- Answering
msg.sender. That is the safe check, used correctly intransferOwnership. - Blaming a leaked key. No key leaked; the owner was tricked into originating the transaction.
- Pointing at
transferor the balance check. Those are fine; the flaw is in how the owner is identified. - Calling it reentrancy. Nothing re-enters; the single call simply passes a phishable guard.
How to defend against it
Identify callers with msg.sender.
- Replace
tx.origin == ownerwithmsg.sender == ownerin every access check. - Avoid
tx.originfor authorization entirely. Its few legitimate uses are discouraged, and account abstraction further erodes the notion of a single origin. - Add tests that call privileged functions through an intermediary contract, so a phishable guard fails in CI.