Why tx.origin is never an authorization check
Using tx.origin to authorize a caller lets any contract the victim touches act on their behalf. The mechanism, a worked example on an auction contract, and the one-word fix.
Updated 2026-09-05
tx.origin and msg.sender look interchangeable in the common case, which is exactly why this bug survives review. When an externally owned account calls a contract directly, they are equal. They diverge the moment a contract sits in between — and an attacker controls whether a contract sits in between.
The difference
msg.senderis the immediate caller — the address that made this specific call. It changes at every hop in a call chain.tx.originis the EOA that signed the transaction that started the whole chain. It is the same value at every depth, no matter how many contracts the call passes through.
The vulnerable contract
A simple English auction. Only the beneficiary should be able to end it and claim the winning bid.
function endAuction() external {
require(tx.origin == beneficiary, "not beneficiary");
require(!ended, "already ended");
ended = true;
payable(beneficiary).transfer(highestBid);
}The attack
The attacker never needs the beneficiary's key. They need the beneficiary to send one transaction to a contract the attacker wrote — any contract, for any reason. A mint page, an airdrop claim, a token approval, a link in a Discord message.
contract Phish {
AuctionHouse public target;
// Anything the victim can be persuaded to call. It does not need to
// look suspicious, and it can do something real as well.
function claimReward() external {
target.endAuction();
}
}When the beneficiary calls claimReward(), the chain is beneficiary → Phish → AuctionHouse. Inside endAuction, msg.sender is the Phish contract — but tx.origin is still the beneficiary, because they signed the outer transaction. The check passes. The auction ends on the attacker's schedule rather than the beneficiary's.
In this particular contract the funds still go to the beneficiary, so the damage is timing rather than theft: an attacker can force settlement at the moment that suits them, ending the auction while their own bid is highest instead of letting it run. That distinction is worth stating precisely, because the same one-word mistake in a contract whose privileged function pays out to a caller-supplied address is a direct drain. The authorization check is broken either way; only the payoff differs.
The fix
function endAuction() external {
require(msg.sender == beneficiary, "not beneficiary");
require(!ended, "already ended");
ended = true;
payable(beneficiary).transfer(highestBid);
}With msg.sender, the intermediate contract is the caller and the check fails — which is the correct outcome, because the beneficiary did not intend to end the auction, and now the code agrees. The rule is unconditional: never use tx.origin for authorization. There is no variant of this pattern that is safe, and Solidity's own documentation says so.
The one arguably defensible use of tx.origin is asserting that the caller is not a contract, via require(msg.sender == tx.origin). Even that is discouraged: it breaks every smart contract wallet, multisig and account-abstraction account — a large and growing share of real users — and account abstraction is steadily eroding the distinction it depends on.