Securite·5 min de lecture·Par Solingo

tx.origin vs msg.sender — Why It Matters for Security

Learn the critical difference between tx.origin and msg.sender, why using tx.origin for authentication is dangerous, and when to use each global variable.

# tx.origin vs msg.sender — Why It Matters for Security

One of the most common security mistakes in Solidity is using tx.origin for authentication. This seemingly innocent choice can expose your contract to phishing attacks that drain user funds.

In this article, we'll clarify the difference between tx.origin and msg.sender, demonstrate the vulnerability, and establish when to use each.

The Difference

msg.sender

The immediate caller of the current function.

  • If a user calls your contract directly: msg.sender = user's address
  • If contract A calls contract B: msg.sender = contract A's address
  • Changes at each call in the call chain

tx.origin

The original external account that initiated the transaction.

  • Always the EOA (Externally Owned Account) that signed the transaction
  • Never a contract address
  • Remains constant throughout the entire call chain

Visual Example

User (0xUser) → Contract A → Contract B → Contract C

| | |

msg.sender: 0xUser A B

tx.origin: 0xUser 0xUser 0xUser

At Contract C:

  • msg.sender = Contract B's address
  • tx.origin = 0xUser (the original EOA)

The Vulnerability: Phishing Attack

Vulnerable Contract

contract VulnerableWallet {

address public owner;

constructor() {

owner = msg.sender;

}

// VULNERABLE: Using tx.origin for authentication

function transfer(address payable to, uint256 amount) public {

require(tx.origin == owner, "Not owner");

to.transfer(amount);

}

receive() external payable {}

}

The Attack

An attacker creates a malicious contract:

contract MaliciousContract {

address payable public attacker;

VulnerableWallet public wallet;

constructor(address _wallet) {

attacker = payable(msg.sender);

wallet = VulnerableWallet(payable(_wallet));

}

// Innocent-looking function

function claimAirdrop() public {

// Calls victim's wallet

wallet.transfer(attacker, address(wallet).balance);

}

}

Attack Flow

  • Attacker deploys MaliciousContract
  • Attacker convinces victim to call claimAirdrop() (via phishing)
  • Victim calls MaliciousContract.claimAirdrop()
  • Malicious contract calls VulnerableWallet.transfer()
  • In the vulnerable wallet:
  • - tx.origin = victim's address ✅ (check passes!)

    - msg.sender = MaliciousContract's address

  • Funds are transferred to attacker
  • Victim loses all funds
  • Why it works: The victim initiated the transaction, so tx.origin equals the owner throughout the entire call chain.

    Secure Implementation

    Using msg.sender

    contract SecureWallet {
    

    address public owner;

    constructor() {

    owner = msg.sender;

    }

    // SECURE: Using msg.sender

    function transfer(address payable to, uint256 amount) public {

    require(msg.sender == owner, "Not owner");

    to.transfer(amount);

    }

    receive() external payable {}

    }

    Now the attack fails:

    • msg.sender = MaliciousContract's address ❌ (check fails)
    • Transaction reverts before funds are stolen

    When to Use Each

    Use msg.sender for:

    Authentication/Authorization

    modifier onlyOwner() {
    

    require(msg.sender == owner, "Not authorized");

    _;

    }

    Access Control

    mapping(address => bool) public whitelist;
    
    

    function restrictedAction() external {

    require(whitelist[msg.sender], "Not whitelisted");

    }

    Sender-Specific Logic

    mapping(address => uint256) public balances;
    
    

    function deposit() external payable {

    balances[msg.sender] += msg.value;

    }

    Use tx.origin for:

    ⚠️ Rare legitimate cases:

  • Gas refunds to original caller
  • function expensiveOperation() external {
    

    // Complex computation

    // Refund gas to original user (not intermediate contract)

    payable(tx.origin).transfer(gasRefund);

    }

  • Analytics/Logging (never for security)
  • event Action(address indexed user, address indexed caller);
    
    

    function logAction() external {

    emit Action(tx.origin, msg.sender);

    }

  • Preventing contract-to-contract calls (use with caution)
  • function onlyEOA() external {
    

    require(tx.origin == msg.sender, "No contract calls");

    // Ensures direct user interaction

    }

    Warning: Blocking contracts prevents account abstraction and future composability.

    Real-World Impact

    THORChain Vulnerability (2021)

    An early version had tx.origin checks that could be exploited via router contracts. Fortunately, it was caught in audit before launch.

    OpenZeppelin Recommendation

    The OpenZeppelin team explicitly warns against tx.origin:

    > "Using tx.origin for authorization may lead to phishing attacks where a malicious contract tricks users into approving transactions."

    EIP-3074 Implications

    Future Ethereum improvements like EIP-3074 (transaction sponsorship) may change tx.origin behavior, making it even riskier to rely on.

    Detection and Prevention

    Slither Detection

    Slither automatically flags tx.origin usage:

    slither . --detect tx-origin

    Output:

    VulnerableWallet.transfer() (contracts/Wallet.sol#8-11) uses tx.origin for authorization:
    

    - require(bool)(tx.origin == owner) (contracts/Wallet.sol#9)

    Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-usage-of-txorigin

    Code Review Checklist

    ✅ All tx.origin occurrences are justified

    ✅ No tx.origin in require() for authorization

    ✅ No tx.origin in access control modifiers

    msg.sender used for all authentication

    Testing

    Write tests that simulate malicious intermediaries:

    // Foundry test
    

    function testPhishingAttack() public {

    // Deploy wallet owned by user

    vm.prank(user);

    VulnerableWallet wallet = new VulnerableWallet();

    vm.deal(address(wallet), 10 ether);

    // Deploy malicious contract

    MaliciousContract attacker = new MaliciousContract(address(wallet));

    // User falls for phishing, calls malicious contract

    vm.prank(user);

    attacker.claimAirdrop();

    // Attacker now has the funds

    assertEq(address(attacker).balance, 10 ether);

    }

    Summary Table

    | Aspect | msg.sender | tx.origin |

    |--------|---------------|--------------|

    | Value | Immediate caller | Original EOA |

    | Can be contract | Yes | No |

    | Changes in call chain | Yes | No |

    | For authentication | ✅ Safe | ❌ Dangerous |

    | For access control | ✅ Recommended | ❌ Vulnerable |

    | For logging | ✅ Use both | ✅ Acceptable |

    | Phishing risk | ❌ No | ✅ High |

    Best Practices

  • Default to msg.sender — it's almost always what you need
  • Never use tx.origin for authentication — no exceptions
  • Audit every tx.origin occurrence — justify its necessity
  • Enable Slither checks — automate detection
  • Test phishing scenarios — assume malicious intermediaries exist
  • Educate your team — this is a common mistake
  • Conclusion

    The choice between tx.origin and msg.sender isn't just a technical detail — it's a critical security decision.

    The rule is simple: Use msg.sender for authentication and authorization. Reserve tx.origin for rare edge cases where you genuinely need the original transaction signer, and never for security-critical checks.

    Master secure authentication patterns on Solingo — our exercises include phishing attack simulations and real-time feedback to help you avoid these dangerous pitfalls.

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement