Skip to content

Use of tx.origin

A natural requirement for smart contracts is to authorize certain parties to perform privileged operations. These could include administrative functions such as changing an admin address, withdrawing funds, or triggering protocol-critical mechanisms. At first glance, Solidity provides two mechanisms to identify the calling party: msg.sender and tx.origin. While they may seem interchangeable in many scenarios, their behavior diverges depending on how the contract is invoked - and misunderstanding this difference can introduce critical vulnerabilities.

To illustrate how these properties differ, consider a setup involving three entities: an admin address initiating a transaction, a target contract that enforces access control, and an optional proxy contract sitting between the admin and the target.

flowchart LR
 Admin -->|msg.sender| Target
 Admin --> Proxy
 Proxy -->|tx.origin| Target

In this example, when the admin calls the target contract directly, both msg.sender and tx.origin yield the same value: the admin's address. However, when using a proxy contract, msg.sender becomes the proxy contract, while tx.origin remains the original admin address. The Solidity documentation describes these values as:

  • msg.sender (address): sender of the message (current call)
  • tx.origin (address): sender of the transaction (full call chain)

The consequence of this distinction is that if a contract uses tx.origin to authorize a caller, it may inadvertently allow malicious proxy contracts to spoof the identity of the original sender. This is possible because smart contracts can invoke other contracts without the user’s explicit approval for each step in the call chain. By relying on tx.origin, a contract effectively trusts the entire call path - not just the direct caller - creating a dangerous surface for phishing-style attacks.

Attack Scenario

Consider a lending protocol that supports user liquidations to make this vulnerability concrete. In its early version, the project uses a LiquidationManager contract to ensure that only an authorized bot or actor can perform liquidations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.8.0;

interface ILendingProtocol {
    function liquidate(address user) external;
}

contract LiquidationManager {
    address public authorizedLiquidator;
    ILendingProtocol public lendingProtocol;

    constructor(address _liquidator, address _lendingProtocol) {
        authorizedLiquidator = _liquidator;
        lendingProtocol = ILendingProtocol(_lendingProtocol);
    }

    // @audit tx.origin for authorization
    function performLiquidation(address _user) external {
        require(tx.origin == authorizedLiquidator, "Not authorized");
        lendingProtocol.liquidate(_user);
    }
}

The contract should ensure that only a specific address can initiate liquidations. However, because it uses tx.origin to perform this check, the protection can be bypassed by an attacker deploying the following contract:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
contract PhishingWrapper {
    LiquidationManager public manager;
    address public targetUser;

    constructor(address _manager, address _targetUser) {
        manager = LiquidationManager(_manager);
        targetUser = _targetUser;
    }

    function bait() external {
        manager.performLiquidation(targetUser);
    }
}

If the authorized liquidator calls the bait() function - perhaps assuming it's a contract peripherahl - the manager contract sees tx.origin as the correct liquidator address and approves the call. In reality, the liquidation is triggered by a malicious proxy, targeting an unintended user. As a result, the attacker gains the ability to perform unauthorized liquidations.

Fragile EOA Detection

In addition to authorization logic, many smart contracts (especially early DeFi projects) try to ensure that only externally owned accounts (EOAs) can interact with them. This is commonly implemented using a simple check:

1
require(tx.origin == msg.sender, "Caller must be EOA");

This check works under the assumption that if tx.origin and msg.sender are the same, the call must have originated from an EOA. However, this assumption is becoming increasingly fragile. With the upcoming Ethereum Pectra upgrade (scheduled for May 7, 2025), EIP-7702 will be introduced, allowing EOAs to transition into smart contract accounts and vice versa.

Through this mechanism - known as self-sponsoring - a smart contract can construct a transaction that makes msg.sender and tx.origin equal, even though the originator is no longer a traditional EOA. This fundamentally breaks the tx.origin == msg.sender assumption. The EIP's security considerations explicitly warn:

Allowing the sender of an EIP-7702 to also set code has the possibility to:

  • Break atomic sandwich protections which rely on tx.origin;
  • Break reentrancy guards of the style require(tx.origin == msg.sender).

While the EIP authors argue that these risks are acceptable in light of broader design goals, the developers' and auditors' responsibility is to recognize, flag, and avoid any anti-pattern using tx.origin. As of 2025, very few legitimate use cases for tx.origin are left, and post-EIP-7702, there will be even fewer ones.