msg.sender vs tx.origin in Solidity

Solidity gives you two built-in variables to identify who is calling your contract: msg.sender and tx.origin. They look similar, but they track different things. Picking the wrong one for access control is one of the most common and exploitable mistakes in Solidity code.

This post explains what each variable does, how they differ in a multi-contract call chain, and why tx.origin is dangerous for authentication.

What Each Variable Tracks

msg.sender is the immediate caller of the current function. If a user calls your contract directly, msg.sender is the user's address. If another contract calls your contract, msg.sender is that contract's address. It changes at every step in the call chain.

tx.origin is the original transaction signer. It always points to the externally owned account (EOA) that signed and submitted the transaction, no matter how many contracts are involved in the call chain. It never changes within a transaction.

How the Call Chain Changes msg.sender

Consider this call sequence:

User (0xUser)
    |
    | calls
    v
Contract A
    |
    | calls
    v
Contract B

Inside Contract B, the two variables resolve to different addresses:

  • msg.sender = address of Contract A
  • tx.origin = 0xUser (the original EOA)

This difference is subtle, but it has serious security consequences.

Here is a minimal contract that records both values:

contract CallerInfo {
    address public lastSender;
    address public lastOrigin;

    function recordCaller() public {
        lastSender = msg.sender;
        lastOrigin = tx.origin;
    }
}

contract Intermediary {
    function callThroughMe(address target) public {
        CallerInfo(target).recordCaller();
    }
}

If a user at 0xUser calls Intermediary.callThroughMe(), which then calls CallerInfo.recordCaller():

  • lastSender will hold the address of the Intermediary contract
  • lastOrigin will hold 0xUser

The Security Problem with tx.origin

Using tx.origin for authentication creates a phishing vulnerability. Any contract that tricks the real owner into calling it can drain funds from your wallet.

Here is a vulnerable wallet:

contract Wallet {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function transfer(address to, uint amount) public {
        require(tx.origin == owner, "Not owner"); // vulnerable
        payable(to).transfer(amount);
    }
}

An attacker deploys this malicious contract:

contract Attack {
    Wallet public wallet;
    address public attacker;

    constructor(address _wallet) {
        wallet = Wallet(_wallet);
        attacker = msg.sender;
    }

    function attack() public {
        // Calls the wallet on behalf of whoever calls this function
        wallet.transfer(attacker, address(wallet).balance);
    }
}

The attack works like this:

  1. The attacker shares the Attack contract with the wallet owner (through a phishing site, a malicious airdrop, etc.)
  2. The owner calls Attack.attack(), thinking they are interacting with some other protocol
  3. Attack.attack() calls Wallet.transfer()
  4. Inside Wallet.transfer(), the check tx.origin == owner passes because the owner did sign the original transaction
  5. Funds are sent to the attacker's address
Owner (tx.origin = owner)
    |
    | tricked into calling
    v
Attack.attack()
    |
    | calls
    v
Wallet.transfer()
    |
    | require(tx.origin == owner) passes
    v
Funds drained

The wallet never knew the owner did not directly authorize this transfer. It only checked who signed the transaction, not who actually called the function.

Never use tx.origin as the sole check for ownership or authorization. Any intermediate contract in the call chain can exploit it.

The Fix: Use msg.sender

Replace tx.origin with msg.sender in the access control check:

contract SafeWallet {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function transfer(address to, uint amount) public {
        require(msg.sender == owner, "Not owner"); // safe
        payable(to).transfer(amount);
    }
}

The same attack now fails:

Owner
    |
    | calls
    v
Attack.attack()
    |
    | calls
    v
SafeWallet.transfer()
    |
    | require(msg.sender == owner) fails
    | (msg.sender is the Attack contract, not the owner)
    v
Transaction reverts

Because msg.sender is the Attack contract's address, not the owner's address, the require check rejects the call.

When tx.origin Is Acceptable

There are narrow, non-security use cases where tx.origin is useful:

Gas tracking in relay contracts. If you are building a meta-transaction relay, you might want to track gas usage by the original end user rather than the relay itself:

contract GasRelay {
    mapping(address => uint) public userGasSpent;

    function relayCall(address target, bytes calldata data) public {
        uint gasBefore = gasleft();

        (bool success,) = target.call(data);
        require(success, "Call failed");

        // Track gas by the original user, not this relay contract
        userGasSpent[tx.origin] += gasBefore - gasleft();
    }
}

Here tx.origin is used only for bookkeeping. The actual access control inside target still uses msg.sender.

Analytics and event logging. If you want to emit an event that records who ultimately initiated a batch of calls, tx.origin lets you capture the end user's address.

In both cases, tx.origin is supplemental information, not a security gate.

Quick Reference

Use caseUse
Ownership checksmsg.sender
Access control and permissionsmsg.sender
Transferring funds or tokensmsg.sender
Any security-critical checkmsg.sender
Logging the originating EOAtx.origin
Gas tracking in relaystx.origin
Meta-transaction patterns (non-auth)tx.origin

Summary

msg.sender reflects the direct caller of your function. tx.origin reflects the EOA that started the entire transaction. For any access control or ownership check, always use msg.sender. Using tx.origin for authentication lets a malicious contract impersonate the real user simply by getting them to call it, even once. Reserve tx.origin for analytics or tracking purposes where security is not at stake.