Why You Should Never Use tx.origin for Authorization
Of all the subtle pitfalls in Solidity, misusing tx.origin for authorization stands out because it looks reasonable at first glance. The value always contains the address of the original externally owned account (EOA) that signed the transaction, so it seems like a trustworthy identity check. In practice, it creates a phishing vector that lets an attacker drain funds or trigger privileged actions without the victim ever realizing they have been exploited. This guide explains exactly why, walks through a concrete attack, and shows you the correct authorization pattern.
tx.origin vs msg.sender: The Core Difference
tx.origin and msg.sender are both global variables in Solidity, but they refer to different things depending on the call stack.
- tx.origin is always the EOA that originally signed and submitted the transaction. It never changes as the call travels through multiple contracts.
- msg.sender is the immediate caller of the current function. When contract A calls contract B, inside B the value of
msg.senderis A's address, not the original EOA.
Consider this call chain: EOA → Contract A → Contract B. Inside Contract B:
tx.originequals the EOA's address.msg.senderequals Contract A's address.
This distinction is the root of the vulnerability. When you guard a function with require(tx.origin == owner), you are asserting that somewhere in the call chain there is a specific EOA, not that the direct caller is trusted. Any contract in that chain can satisfy the check as long as it tricks the right EOA into making a transaction.
The Phishing Attack, Step by Step
Imagine a simple wallet contract with a transfer function guarded by tx.origin:
// VULNERABLE: do not use this pattern
contract TxOriginWallet {
address public owner;
constructor() {
owner = msg.sender;
}
function transfer(address payable recipient, uint256 amount) external {
// BUG: uses tx.origin instead of msg.sender
require(tx.origin == owner, "Not owner");
recipient.transfer(amount);
}
receive() external payable {}
}
Now consider an attacker who deploys the following malicious contract and tricks the wallet owner into calling it. The lure could be anything: a fake airdrop, a token approval for a DeFi protocol, a game transaction. The social-engineering angle is the same as any phishing attack.
// ATTACKER CONTRACT
contract PhishingContract {
TxOriginWallet private target;
address payable private attacker;
constructor(TxOriginWallet _target, address payable _attacker) {
target = _target;
attacker = _attacker;
}
// Victim calls this function, thinking it is harmless
function claimAirdrop() external {
// At this point:
// tx.origin == victim (the wallet owner)
// msg.sender == this contract
// The wallet's require(tx.origin == owner) passes!
uint256 balance = address(target).balance;
target.transfer(attacker, balance);
}
}
When the wallet owner calls claimAirdrop(), the EVM executes the following call chain:
- EOA (owner) calls
PhishingContract.claimAirdrop(). PhishingContractcallsTxOriginWallet.transfer(attacker, balance).- Inside
TxOriginWallet.transfer,tx.originis still the owner's EOA address. Therequirepasses. - The entire wallet balance is sent to the attacker.
The victim signed exactly one legitimate transaction. They never approved a transfer. The authorization check passed correctly according to its own flawed logic, and there is no on-chain evidence of deception beyond the call trace.
The Correct Authorization Pattern
The fix is straightforward: replace tx.origin with msg.sender for all authorization checks. The direct caller is the party you have a contractual relationship with, not whoever originally initiated the transaction.
// SAFE: use msg.sender for authorization
contract SafeWallet {
address public owner;
constructor() {
owner = msg.sender;
}
function transfer(address payable recipient, uint256 amount) external {
require(msg.sender == owner, "Not owner");
recipient.transfer(amount);
}
receive() external payable {}
}
Now the same phishing attack fails. When PhishingContract calls SafeWallet.transfer, msg.sender inside the wallet is PhishingContract's address, not the owner's address. The require reverts the call immediately.
When Is tx.origin Acceptable?
There is exactly one common, legitimate use case: asserting that the caller is not a contract. The check require(tx.origin == msg.sender) reverts if the immediate caller is a contract rather than an EOA. This pattern is used in some contexts to block flash loan attacks or prevent certain composability patterns. Note, however, that this check is not a security silver bullet. Account abstraction wallets (ERC-4337) involve smart contract accounts as the transaction origin, so this guard breaks compatibility with them and is increasingly considered an antipattern in modern Solidity design.
For role-based and ownership-based authorization, always use msg.sender. For more complex scenarios, consider battle-tested libraries such as OpenZeppelin's Ownable and AccessControl, which are built entirely around msg.sender. You can read more about the broader landscape of permission bugs in our guide to Access Control Vulnerabilities in Smart Contracts.
How Automated Tools Catch tx.origin Misuse
Static analysis tools are highly effective at detecting this specific pattern because it reduces to a simple AST query: find any comparison involving tx.origin inside a conditional that guards state-changing logic.
- Slither includes a dedicated
tx-origindetector that flags every use oftx.originin a conditional and reports the exact source location. - Solhint raises a linting warning when
tx.originappears in expressions used for access control. - Semgrep can match the pattern with custom rules targeting
requireandifconditions that referencetx.origin.
Because this is a well-defined syntactic pattern, automated scanning reliably surfaces it before deployment. If you want to verify whether your codebase contains any tx.origin authorization checks alongside other common vulnerabilities, you can run an automated scan against your contracts and review each finding with a plain-language explanation of the risk.
It is worth noting that while automated scanning catches known patterns effectively, it does not replicate the judgment a human auditor applies to novel logic flaws or complex business logic. Treat automated results as a baseline that eliminates known vulnerabilities before deeper review.
Related Vulnerability: Unchecked External Calls
The phishing scenario above involves an external contract call that the victim initiates unwittingly. A related class of bugs concerns what happens when your own contract makes external calls and does not verify the result. If the called contract behaves unexpectedly or reverts silently, your contract may proceed in an inconsistent state. For a detailed treatment of that risk, see our guide on Unchecked External Calls and the Risks of call, send and transfer.
Summary
The tx.origin vulnerability is deceptive precisely because the global variable does contain a real, authenticated address. The flaw is not in the data itself but in what that data actually represents: the origin of a transaction, not the identity of a trusted caller. Any contract in the call stack can exploit an authorization check based on tx.origin as long as it can trick the right EOA into making any transaction.
- Use
msg.senderfor all ownership and role-based authorization. - Reserve
tx.origin == msg.senderonly for EOA-only guards, and understand the account abstraction compatibility trade-off. - Run static analysis as part of your CI pipeline to catch this and similar patterns before they reach mainnet.
Scan your contract before you ship
Run an automated, transparent security scan — seven industry tools in parallel, every finding labeled with its source tool. It is not a substitute for a full manual audit, but it is a fast first line of defense.
Scan a contractFrequently asked questions
What is the difference between tx.origin and msg.sender in Solidity?
tx.origin always holds the address of the externally owned account (EOA) that originally signed the transaction. msg.sender holds the address of the immediate caller of the current function, which may be a contract rather than an EOA. In a direct call from an EOA to a contract, the two values are equal. Once an intermediate contract is involved, they diverge: tx.origin remains the original EOA, while msg.sender becomes the intermediate contract's address.
Why is using tx.origin for authorization a security vulnerability?
Because any contract in the call chain can exploit the check. If your contract requires tx.origin == owner, a malicious contract can call your function on behalf of the owner simply by tricking the owner into calling the malicious contract first. The authorization check passes because the original transaction signer is still the owner, even though the actual caller is an untrusted third-party contract. The result can be unauthorized fund transfers or privileged state changes.
Is there any legitimate use for tx.origin?
The most common legitimate use is the pattern require(tx.origin == msg.sender), which checks that the immediate caller is an EOA rather than a contract. This blocks certain flash loan or contract-based attack vectors. However, this pattern is incompatible with ERC-4337 smart contract wallets, where the account itself is a contract. For authorization purposes such as ownership checks, msg.sender is always the correct choice.
Can static analysis tools detect tx.origin misuse automatically?
Yes. Tools like Slither have a dedicated tx-origin detector that flags every conditional comparison involving tx.origin in state-changing code paths. Solhint also warns about the pattern at the linting level. Because the vulnerability has a well-defined syntactic signature, automated tools catch it reliably, making it one of the easier classes of bugs to eliminate before deployment.
Does replacing tx.origin with msg.sender fully prevent phishing attacks on smart contracts?
Using msg.sender closes the specific authorization bypass described here. However, it does not eliminate all phishing risk. Users can still be tricked into calling a malicious contract that abuses legitimate approvals they have previously granted, such as token allowances. Defense in depth includes time-locked operations, multi-signature schemes, minimal permission scopes, and educating users to verify contract addresses before signing transactions.