Understanding tx.origin vs msg.sender
Before diving into the attack, it's crucial to understand the difference between these two global variables that identify who is calling your contract.
msg.sender
The immediate caller of the current function. This changes with each call in the chain.
- • Can be an EOA (user wallet)
- • Can be a contract
- • Changes at each call level
- • Safe for authentication
tx.origin
The original sender who initiated the transaction. This never changes, regardless of how many contracts are called.
- • Always an EOA (user wallet)
- • Never a contract
- • Same throughout entire tx
- • Dangerous for authentication
🔗 Call Chain Visualization
Initiates transaction
First contract called
Called by Contract A
How the Attack Works
The tx.origin phishing attack exploits the fact that tx.origin doesn't change when a contract makes a call. An attacker tricks a victim into interacting with a malicious contract, which then calls the victim's contract using the victim's identity.
🎣 Phishing Attack Flow
Attacker Deploys Malicious Contract
The attacker creates a contract that looks innocent (e.g., airdrop claim, NFT mint, or fake DeFi protocol).
Victim Interacts with Malicious Contract
Through social engineering (fake website, Discord link, etc.), the victim is tricked into calling a function on the malicious contract.
Malicious Contract Calls Victim's Wallet Contract
The malicious contract internally calls the victim's wallet (or any contract that uses tx.origin for auth).
tx.origin Check Passes!
Since tx.origin is still the victim's address, the wallet contract thinks the legitimate owner is making the call. Funds are stolen.
Complete Attack Scenario
Let's walk through a complete attack with code. Imagine a simple wallet contract that uses tx.origin to verify the owner:
1// 🚨 VULNERABLE: Wallet using tx.origin for authentication
2contract VulnerableWallet {
3 address public owner;
4
5 constructor() {
6 owner = msg.sender;
7 }
8
9 // 💀 DANGEROUS: Uses tx.origin for authentication
10 function withdraw(address to, uint256 amount) external {
11 require(tx.origin == owner, "Not owner"); // 🚨 BUG!
12
13 (bool success,) = to.call{value: amount}("");
14 require(success, "Transfer failed");
15 }
16
17 receive() external payable {}
18}Now the attacker deploys a phishing contract:
1// 🎣 ATTACKER: Phishing contract disguised as legitimate service
2contract PhishingContract {
3 VulnerableWallet public targetWallet;
4 address public attacker;
5
6 constructor(address _wallet) {
7 targetWallet = VulnerableWallet(payable(_wallet));
8 attacker = msg.sender;
9 }
10
11 // Disguised as "claim airdrop" or "mint NFT"
12 function claimReward() external {
13 // Victim thinks they're claiming a reward...
14 // But this actually calls the victim's wallet!
15
16 // tx.origin is still the victim, so this works!
17 targetWallet.withdraw(
18 attacker,
19 address(targetWallet).balance
20 );
21 }
22
23 // Or hidden in a fallback/receive function
24 receive() external payable {
25 // Even just receiving ETH can trigger the attack!
26 if (address(targetWallet).balance > 0) {
27 targetWallet.withdraw(attacker, address(targetWallet).balance);
28 }
29 }
30}Attack Execution
- Alice deploys VulnerableWallet and deposits 10 ETH
- Attacker deploys PhishingContract pointing to Alice's wallet
- Attacker sends Alice a fake "Free NFT" link
- Alice calls claimReward() on the phishing contract
- tx.origin = Alice, so the wallet thinks Alice is calling
- All 10 ETH transferred to attacker! 💸
1// Attack flow in Foundry test
2function test_TxOriginPhishing() public {
3 // Setup: Alice creates wallet with 10 ETH
4 vm.prank(alice);
5 VulnerableWallet wallet = new VulnerableWallet();
6 vm.deal(address(wallet), 10 ether);
7
8 // Attacker deploys phishing contract
9 vm.prank(attacker);
10 PhishingContract phishing = new PhishingContract(address(wallet));
11
12 uint256 attackerBalanceBefore = attacker.balance;
13
14 // 🎣 Alice is tricked into calling the phishing contract
15 // She thinks she's claiming an airdrop
16 vm.prank(alice); // tx.origin = alice
17 phishing.claimReward();
18
19 // 💀 All funds stolen!
20 assertEq(address(wallet).balance, 0);
21 assertEq(attacker.balance, attackerBalanceBefore + 10 ether);
22}Vulnerable Code Patterns
❌ Pattern 1: Direct tx.origin Check
1// 🚨 VULNERABLE
2contract BadContract {
3 address owner;
4
5 modifier onlyOwner() {
6 require(tx.origin == owner, "Not owner"); // 💀 WRONG!
7 _;
8 }
9
10 function sensitiveAction() external onlyOwner {
11 // Can be called by any contract when owner interacts with it
12 }
13}❌ Pattern 2: tx.origin in Constructor
1// 🚨 VULNERABLE: Can be exploited via factory patterns
2contract BadFactory {
3 address public admin;
4
5 constructor() {
6 admin = tx.origin; // 💀 If deployed by another contract, wrong admin!
7 }
8}❌ Pattern 3: tx.origin for Multi-sig
1// 🚨 VULNERABLE: Multi-sig with tx.origin
2contract BadMultiSig {
3 mapping(address => bool) public isOwner;
4
5 function executeTransaction(address to, uint256 value) external {
6 // 💀 Any owner interacting with ANY contract can trigger this
7 require(isOwner[tx.origin], "Not owner");
8
9 (bool success,) = to.call{value: value}("");
10 require(success);
11 }
12}❌ Pattern 4: Mixed Usage (Still Dangerous)
1// 🚨 VULNERABLE: Mixing tx.origin with msg.sender doesn't help
2contract StillVulnerable {
3 address owner;
4
5 function withdraw() external {
6 // This check is useless if the next one uses tx.origin
7 require(msg.sender != address(0), "Zero sender");
8
9 // 💀 Still vulnerable!
10 require(tx.origin == owner, "Not owner");
11
12 // Drain funds...
13 }
14}Real-World Context
While tx.origin phishing is well-documented and taught in every Solidity course, it remains a risk because:
Legacy Contracts
Ongoing RiskMany early Ethereum contracts (2015-2017) used tx.origin before the vulnerability was widely understood. Some still hold significant value.
Smart Contract Wallets
Account Abstraction EraWith ERC-4337 and smart contract wallets becoming common, tx.origin-based checks become even more problematic as the transaction initiator may differ from the account owner.
CTF Challenges & Education
Learningtx.origin phishing is featured in Ethernaut (Level 4), Damn Vulnerable DeFi, and other CTFs because it's a fundamental concept every developer must understand.
Auditor Findings
Common in AuditsSecurity auditors regularly flag tx.origin usage. It's considered a high-severity finding by most audit firms and automated tools like Slither and Mythril.
Why This Still Matters
Prevention Strategies
Use msg.sender
Always use msg.sender for authentication. It represents the immediate caller, which is what you actually want to verify.
Use OpenZeppelin
Use OpenZeppelin's Ownable, AccessControl, or other auth patterns. They're audited and never use tx.origin.
Use Static Analysis
Tools like Slither automatically detect tx.origin usage. Add them to your CI/CD pipeline.
Test Attack Scenarios
Write tests that simulate phishing attacks. If your contract can be exploited via intermediary contracts, you'll catch it.
✅ Correct Implementation
1// ✅ SECURE: Using msg.sender for authentication
2contract SecureWallet {
3 address public owner;
4
5 constructor() {
6 owner = msg.sender; // ✅ msg.sender is fine in constructor
7 }
8
9 modifier onlyOwner() {
10 require(msg.sender == owner, "Not owner"); // ✅ CORRECT!
11 _;
12 }
13
14 function withdraw(address to, uint256 amount) external onlyOwner {
15 (bool success,) = to.call{value: amount}("");
16 require(success, "Transfer failed");
17 }
18
19 function transferOwnership(address newOwner) external onlyOwner {
20 require(newOwner != address(0), "Zero address");
21 owner = newOwner;
22 }
23
24 receive() external payable {}
25}
26
27// Even better: Use OpenZeppelin
28import "@openzeppelin/contracts/access/Ownable.sol";
29
30contract BetterWallet is Ownable {
31 constructor() Ownable(msg.sender) {}
32
33 function withdraw(address to, uint256 amount) external onlyOwner {
34 (bool success,) = to.call{value: amount}("");
35 require(success, "Transfer failed");
36 }
37
38 receive() external payable {}
39}✅ Preventing Phishing Attack
1// With msg.sender, the phishing attack FAILS
2contract PhishingContract {
3 SecureWallet public targetWallet;
4 address public attacker;
5
6 constructor(address _wallet) {
7 targetWallet = SecureWallet(payable(_wallet));
8 attacker = msg.sender;
9 }
10
11 function attemptPhishing() external {
12 // This will REVERT!
13 // msg.sender in SecureWallet will be THIS contract, not the victim
14 // "Not owner" error will be thrown
15 targetWallet.withdraw(attacker, address(targetWallet).balance);
16 }
17}
18
19// Test showing the attack fails
20function test_PhishingFails() public {
21 // Alice creates secure wallet
22 vm.prank(alice);
23 SecureWallet wallet = new SecureWallet();
24 vm.deal(address(wallet), 10 ether);
25
26 // Attacker deploys phishing contract
27 vm.prank(attacker);
28 PhishingContract phishing = new PhishingContract(address(wallet));
29
30 // Alice is tricked into calling phishing contract
31 vm.prank(alice);
32
33 // ✅ This reverts! Funds are SAFE!
34 vm.expectRevert("Not owner");
35 phishing.attemptPhishing();
36
37 // Wallet still has all funds
38 assertEq(address(wallet).balance, 10 ether);
39}Legitimate Uses of tx.origin
While tx.origin should never be used for authentication, there are a few legitimate use cases:
1// ✅ LEGITIMATE: Preventing contracts from calling
2contract NoContractCalls {
3 // Ensure only EOAs can call, not contracts
4 // Useful for some anti-bot mechanisms
5 modifier onlyEOA() {
6 require(tx.origin == msg.sender, "No contracts allowed");
7 _;
8 }
9
10 function humanOnlyAction() external onlyEOA {
11 // Only externally owned accounts can call this
12 // Note: This is NOT foolproof (constructor calls bypass this)
13 }
14}
15
16// ✅ LEGITIMATE: Gas refunds to transaction initiator
17contract GasRefunder {
18 function expensiveOperation() external {
19 uint256 gasStart = gasleft();
20
21 // ... do expensive work ...
22
23 // Refund gas to the actual user who paid for the tx
24 uint256 gasUsed = gasStart - gasleft();
25 uint256 refund = gasUsed * tx.gasprice;
26
27 // tx.origin is appropriate here - refund goes to whoever paid
28 payable(tx.origin).transfer(refund);
29 }
30}
31
32// ✅ LEGITIMATE: Logging/analytics (non-security-critical)
33contract Analytics {
34 event UserAction(address indexed txOrigin, address indexed caller, string action);
35
36 function doSomething() external {
37 // Using tx.origin for analytics is fine
38 // It's not used for any security decisions
39 emit UserAction(tx.origin, msg.sender, "doSomething");
40
41 // Security-critical logic uses msg.sender
42 require(msg.sender == owner, "Not authorized");
43 }
44}⚠️ EOA Check Limitations
tx.origin == msg.sender check to prevent contract calls can be bypassed by calling from a contract's constructor (during deployment, the contract has no code yet). This is not a reliable anti-bot mechanism.Testing for tx.origin Issues
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.19;
3
4import "forge-std/Test.sol";
5
6// Contract to test
7contract TargetContract {
8 address public owner;
9 uint256 public value;
10 bool public useTxOrigin; // Toggle for testing both
11
12 constructor(bool _useTxOrigin) {
13 owner = msg.sender;
14 useTxOrigin = _useTxOrigin;
15 }
16
17 function setValue(uint256 _value) external {
18 if (useTxOrigin) {
19 require(tx.origin == owner, "Not owner"); // 🚨 Vulnerable
20 } else {
21 require(msg.sender == owner, "Not owner"); // ✅ Secure
22 }
23 value = _value;
24 }
25}
26
27// Attacker contract for phishing simulation
28contract AttackerContract {
29 TargetContract public target;
30
31 constructor(address _target) {
32 target = TargetContract(_target);
33 }
34
35 function attack(uint256 maliciousValue) external {
36 target.setValue(maliciousValue);
37 }
38}
39
40contract TxOriginTest is Test {
41 address owner = makeAddr("owner");
42 address attacker = makeAddr("attacker");
43
44 function test_TxOriginVulnerable() public {
45 // Deploy vulnerable contract
46 vm.prank(owner);
47 TargetContract vulnerable = new TargetContract(true); // Uses tx.origin
48
49 // Deploy attacker contract
50 vm.prank(attacker);
51 AttackerContract attackerContract = new AttackerContract(address(vulnerable));
52
53 // 🎣 Owner is tricked into calling attacker's contract
54 vm.prank(owner);
55 attackerContract.attack(999); // Phishing!
56
57 // 💀 Attack succeeded - value was changed through intermediary
58 assertEq(vulnerable.value(), 999);
59 }
60
61 function test_MsgSenderSecure() public {
62 // Deploy secure contract
63 vm.prank(owner);
64 TargetContract secure = new TargetContract(false); // Uses msg.sender
65
66 // Deploy attacker contract
67 vm.prank(attacker);
68 AttackerContract attackerContract = new AttackerContract(address(secure));
69
70 // 🎣 Owner is tricked into calling attacker's contract
71 vm.prank(owner);
72
73 // ✅ Attack FAILS - msg.sender is the attacker contract, not owner
74 vm.expectRevert("Not owner");
75 attackerContract.attack(999);
76
77 // Value unchanged
78 assertEq(secure.value(), 0);
79 }
80
81 function test_DirectCallStillWorks() public {
82 vm.prank(owner);
83 TargetContract secure = new TargetContract(false);
84
85 // Direct calls by owner still work
86 vm.prank(owner);
87 secure.setValue(42);
88
89 assertEq(secure.value(), 42);
90 }
91
92 // Fuzz test: no intermediary should be able to impersonate owner
93 function testFuzz_NoImpersonation(address intermediary, uint256 value) public {
94 vm.assume(intermediary != owner);
95 vm.assume(intermediary != address(0));
96
97 vm.prank(owner);
98 TargetContract secure = new TargetContract(false);
99
100 // Create intermediary contract
101 vm.prank(intermediary);
102 AttackerContract attackerContract = new AttackerContract(address(secure));
103
104 // Owner calling through intermediary should fail
105 vm.prank(owner);
106 vm.expectRevert("Not owner");
107 attackerContract.attack(value);
108 }
109}🔧 Detection Tools
Slither
slither . --detect tx-originMythril
myth analyze Contract.solSolhint
solhint "**/*.sol"Aderyn
aderyn .✅ tx.origin Safety Checklist
📚 Quick Reference
| Aspect | tx.origin | msg.sender |
|---|---|---|
| Definition | Original transaction sender | Immediate caller |
| Can be contract? | ❌ Never (always EOA) | ✅ Yes |
| Changes in call chain? | ❌ No (stays same) | ✅ Yes (updates per call) |
| Safe for auth? | ❌ No | ✅ Yes |
| Phishing vulnerable? | ⚠️ Yes | ✅ No |