msg.sender vs tx.origin — the rule of thumb

If A → B → C (A calls B, B calls C):

tx.origin tracks the originating EOA across the whole call chain. msg.sender tracks the direct caller.


Why tx.origin is dangerous for access control (phishing attack)

If you protect a function with require(tx.origin == owner), you think “only owner can run this” — but you actually allow any contract to be the immediate caller as long as the transaction was started by the owner.

An attacker can lure the owner into calling a malicious contract (e.g., via a phishing link / malicious UI). That malicious contract then calls the vulnerable contract — and since the tx.origin is the owner, the vulnerable contract allows the action. Boom: owner has been tricked into authorizing the attacker.

Vulnerable example

// BAD: Uses tx.origin for auth
pragma solidity ^0.8.19;

contract Vulnerable {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

    // Suppose only owner should call this
    function withdrawAll() external {
        require(tx.origin == owner, "Not owner"); // <-- vulnerable check
        payable(owner).transfer(address(this).balance);
    }

    // allow the contract to receive some funds
    receive() external payable {}
}

Attack scenario

  1. Owner (an EOA) is tricked into visiting attacker UI and calling Attacker.start() (or sending a tx to attacker).
  2. Attacker contract's start() calls vulnerable.withdrawAll() on behalf of the owner.
  3. Inside Vulnerable, tx.origin is the unknowing owner — the require passes — funds drained.

Exploit contract

pragma solidity ^0.8.19;

interface IVulnerable {
    function withdrawAll() external;
}

contract Attacker {
    IVulnerable public victim;

    constructor(address _victim) {
        victim = IVulnerable(_victim);
    }

    // Owner is tricked into calling this function (owner signs/initiates tx to this contract)
    function start() external {
        // This call runs in one transaction; tx.origin is the EOA that initiated it.
        victim.withdrawAll(); // inside victim, tx.origin == owner -> passes
    }
}

Notice: the owner intentionally sent a transaction (the owner thought they were calling Attacker.start for something innocent). That very action lets Attacker call the victim and succeed.