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 ofContract Atx.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():
lastSenderwill hold the address of theIntermediarycontractlastOriginwill hold0xUser
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:
- The attacker shares the
Attackcontract with the wallet owner (through a phishing site, a malicious airdrop, etc.) - The owner calls
Attack.attack(), thinking they are interacting with some other protocol Attack.attack()callsWallet.transfer()- Inside
Wallet.transfer(), the checktx.origin == ownerpasses because the owner did sign the original transaction - 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.originas 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 case | Use |
|---|---|
| Ownership checks | msg.sender |
| Access control and permissions | msg.sender |
| Transferring funds or tokens | msg.sender |
| Any security-critical check | msg.sender |
| Logging the originating EOA | tx.origin |
| Gas tracking in relays | tx.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.