Why You Should Never Use tx.origin for Authentication in Solidity

tx.origin is the OG Solidity footgun: it points to the human who started the transaction, not the contract calling you. One phishing click and an attacker drains your wallet using your own signature. Use msg.sender. Always.

tx.origin returns the externally-owned account that originated the transaction, while msg.sender returns the immediate caller. Using tx.origin for authentication lets any contract a user interacts with impersonate them: a malicious contract calls your wallet, your wallet checks tx.origin (the user), and the check passes. The fix is trivial — replace require(tx.origin == owner) with require(msg.sender == owner). tx.origin should only be used for transparency logging or for explicitly rejecting contract callers, never for access control. SWC-115.
··6 min read

The Bug in One Sentence

`tx.origin` is the address that *kicked off* the transaction. `msg.sender` is whoever is calling you *right now*. If you authenticate with the former, anyone your user touches can drain them.

This is SWC-115, and it has been in Solidity's footgun hall of fame since 2016. It still ships in production code in 2025, usually because somebody copy-pasted from a tutorial written by a person who shouldn't be writing tutorials.

How the Phishing Attack Works

Imagine a personal wallet contract that lets the owner transfer funds:

```solidity
// VULNERABLE — do not deploy
contract Wallet {
address public owner;

constructor() {
owner = msg.sender;
}

function transfer(address payable to, uint256 amount) public {
require(tx.origin == owner, "not owner");
to.transfer(amount);
}
}
```

Looks fine, right? The owner is the only person who can transfer. Now an attacker deploys this:

```solidity
contract Phish {
Wallet public victimWallet;
address payable public attacker;

constructor(Wallet _w, address payable _a) {
victimWallet = _w;
attacker = _a;
}

// Bait: free NFT mint, airdrop claim, whatever
function claimAirdrop() external {
victimWallet.transfer(attacker, address(victimWallet).balance);
}
}
```

The attacker shills `claimAirdrop()` on Twitter. The owner of `Wallet` calls it. Now the call stack is:

`tx.origin` = victim (the EOA that signed the tx)
- `msg.sender` inside `Wallet.transfer` = `Phish` contract

The `require(tx.origin == owner)` check **passes**, because `tx.origin` is still the victim. The wallet drains itself to the attacker. The victim signed nothing about a transfer. They thought they were minting a JPEG.

This is the entire bug class. Every contract that uses `tx.origin` for auth is a phishing kit waiting for a victim.

The Fix

Replace it with `msg.sender`:

```solidity
// FIXED
contract Wallet {
address public owner;

constructor() {
owner = msg.sender;
}

function transfer(address payable to, uint256 amount) public {
require(msg.sender == owner, "not owner");
to.transfer(amount);
}
}
```

Now when `Phish` calls `transfer`, `msg.sender` is the `Phish` contract, not the victim. The check fails. The victim still loses some gas, but their funds are safe.

That's it. One word. The compiler even warns you about this since Solidity 0.7 if you crank up the linter, and tools like Slither flag it as a high-severity issue by default. If you're not running [a free AI audit](https://www.cryptohawking.com/audit) on every commit, you should be — this is the kind of thing static analysis catches in under a second.

When Is tx.origin Actually OK?

There are exactly two legitimate uses I've seen in five years of auditing:

1. **Refunds and gas rebates.** You want to send gas back to the human, not the relayer contract. `tx.origin` is fine here because you're paying them, not authorizing them.
2. **Explicitly rejecting contract callers.** `require(tx.origin == msg.sender)` ensures the caller is an EOA, not a contract. This was popular for limiting flash-loan-style sandwich attacks. But ERC-4337 account abstraction breaks this assumption — a legitimate smart wallet user will fail your check. In 2025, this pattern is increasingly hostile to real users. Use commit-reveal or per-block rate limits instead.

Everything else: use `msg.sender`.

Real-World Incidents

There's no single billion-dollar `tx.origin` hack the way there's a famous reentrancy hack, because the bug is so well-known that mainstream protocols stopped shipping it years ago. Where it does show up:

**Personal wallet contracts and "smart" multisigs written by individuals**, especially before 2020. Vitalik Buterin himself warned about this pattern in 2016 on the Ethereum Stack Exchange and it became the canonical phishing vector example.
- **NFT mint contracts** with custom `onlyOwner`-style modifiers built by teams who learned Solidity from a YouTube video. I've personally flagged this in three audits in the last 18 months, each time on contracts holding mid-six-figure treasuries.
- **Forked DeFi protocols** where the original used `msg.sender` and the fork "improved" it. Reader, they did not improve it.

The attacks are quiet because they're targeted — phishing one whale doesn't make headlines like a $600M bridge exploit. But the cumulative losses across years of low-key drains are real.

Detection Checklist

Before you ship, grep your repo:

```
grep -rn "tx.origin" contracts/
```

For every hit, ask: *is this an authorization check?* If yes, it's a bug. If it's a refund destination or an EOA gate (and you've thought about ERC-4337 users), it might be fine. Document why.

Automated tools that catch this:

**Slither**: `tx-origin` detector, runs in seconds
- **Mythril**: flags it under SWC-115
- **Solhint**: `avoid-tx-origin` rule
- **Solidity compiler**: emits a warning when you use `tx.origin` in a comparison

If none of those are in your CI, add them today. If you want a human to sanity-check before mainnet, a [manual audit from us](https://www.cryptohawking.com/audit/manual) runs $5,000 in ETH/SOL/USDT with a three business day turnaround — cheap insurance compared to a drained treasury.

TL;DR

`tx.origin` = the human at the start of the call chain.
- `msg.sender` = whoever is calling your function right now.
- Use `msg.sender` for authentication. Always.
- The only honest uses of `tx.origin` are gas refunds and explicit EOA-only gates (and even the latter is increasingly broken in an account-abstraction world).
- This is a fifteen-second fix. Don't be the one whose users get phished because you skipped it.

FAQ

What's the actual difference between tx.origin and msg.sender?

`tx.origin` is always the externally-owned account (EOA) that signed and broadcast the transaction. It never changes during execution. `msg.sender` is whoever invoked the current function — it can be an EOA, but if a contract calls your contract, `msg.sender` is that contract's address. In a call chain A → B → C, inside C, `tx.origin` is A and `msg.sender` is B. For authentication you almost always want the immediate caller, which is `msg.sender`.

Is tx.origin == msg.sender a safe way to block contract callers?

It used to be the standard trick to enforce EOA-only access, and it works against contract-based attacks like flash loans. But ERC-4337 account abstraction means many legitimate users now interact through smart contract wallets where `tx.origin` is a bundler/EntryPoint, not the user. Your check will lock them out. If you need bot or sandwich protection, prefer per-block rate limits, commit-reveal schemes, or signature-based allowlists rather than this brittle pattern.

Will the Solidity compiler warn me about tx.origin?

The compiler emits a warning when `tx.origin` appears in certain comparisons, but it's just a warning — your code still compiles and deploys. Tools like Slither, Mythril, and Solhint flag it more aggressively, often as high severity. Don't rely on warnings alone; add a linter to CI and fail the build on any `tx.origin` usage that isn't explicitly justified with a code comment. SWC-115 is one of the cheapest classes of bugs to eliminate from a codebase.

Can multisigs or proxies be drained via tx.origin?

Only if they use `tx.origin` for authorization, which reputable implementations (Gnosis Safe, OpenZeppelin Ownable, transparent/UUPS proxies) do not. The risk lives in custom or forked wallet contracts. If you've forked a multisig and added "convenience" modifiers, audit those modifiers carefully. Any path where a function checks `tx.origin == owner` instead of `msg.sender == owner` is an open door for a phishing contract to impersonate the owner the moment they click the wrong link.

How do I migrate a deployed contract that uses tx.origin?

You can't patch a deployed non-upgradeable contract. Options: (1) if it's behind a proxy, push an upgrade swapping `tx.origin` for `msg.sender`; (2) if it's immutable, deploy a fixed version, pause the old one if you have that hook, and migrate state/funds; (3) if you can't pause, at minimum warn users publicly and drain treasury funds to a safe contract before an attacker baits a holder. Going forward, run every commit through static analysis so this never reaches mainnet again.

One Solidity tip + 1 case study per month

Why You Should Never Use tx.origin for Authentication in Solidity | Crypto Hawking