tx.origin Authentication Remediation
How to eliminate tx.origin authentication vulnerabilities by replacing all tx.origin identity checks with msg.sender, which correctly identifies the immediate caller and is not vulnerable to phishing via intermediary contracts.
tx.origin Authentication Remediation
Overview
tx.origin authentication vulnerabilities arise when a contract uses require(tx.origin == owner) to authorize a caller. While tx.origin always refers to the original externally-owned account (EOA) that signed the transaction, it does not identify the immediate caller. When a user interacts with a legitimate-looking intermediary contract (an airdrop claim, a DEX aggregator, an NFT mint), that contract becomes msg.sender while the user’s address remains tx.origin.
An attacker exploits this by deploying a phishing contract disguised as a legitimate interaction. When the contract owner calls it, the phishing contract immediately calls the victim wallet. Inside the victim: tx.origin == owner passes, but msg.sender is the attacker’s phishing contract. Funds are drained to the attacker.
Multiple wallet contracts have been drained through this phishing vector. The fix is always to use msg.sender for identity checks and to use tx.origin only for the narrow purpose of detecting whether a call originates directly from an EOA (i.e., that no intermediary contract is involved).
Related Detector: Access Control Detector
Recommended Fix
Before (Vulnerable)
contract VulnerableWallet {
address public owner;
constructor() {
owner = msg.sender;
}
// VULNERABLE: tx.origin can be the owner even when msg.sender is an attacker contract
function withdraw(address payable recipient, uint256 amount) external {
require(tx.origin == owner, "Not owner"); // WRONG: checks original signer, not caller
require(amount <= address(this).balance, "Insufficient balance");
recipient.transfer(amount);
}
receive() external payable {}
}
// ATTACKER: Phishing contract — appears to be an "airdrop" or "NFT mint"
contract PhishingAttack {
VulnerableWallet public victim;
address payable public attacker;
constructor(address _victim, address payable _attacker) {
victim = VulnerableWallet(payable(_victim));
attacker = _attacker;
}
// Owner calls this thinking it is a legitimate interaction
function claimAirdrop() external {
// tx.origin: owner (original signer)
// msg.sender for the victim call: this phishing contract
// require(tx.origin == owner) PASSES — drains the wallet
victim.withdraw(attacker, address(victim).balance);
}
}
After (Fixed)
contract SafeWallet {
address public owner;
constructor() {
owner = msg.sender;
}
// SAFE: msg.sender is the IMMEDIATE caller
// If called through an intermediary, msg.sender = intermediary (not owner) → reverts
function withdraw(address payable recipient, uint256 amount) external {
require(msg.sender == owner, "Not owner"); // CORRECT: checks direct caller
require(recipient != address(0), "Invalid recipient");
require(amount > 0 && amount <= address(this).balance, "Invalid amount");
(bool success,) = recipient.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawn(recipient, amount);
}
receive() external payable {}
event Withdrawn(address indexed recipient, uint256 amount);
}
With msg.sender authentication, the phishing attack fails:
Transaction: Owner → PhishingAttack → SafeWallet.withdraw()
Inside SafeWallet.withdraw():
tx.origin = owner (original signer)
msg.sender = PhishingAttack contract (immediate caller)
require(msg.sender == owner)
→ msg.sender is PhishingAttack, not owner → REVERTS
Alternative Mitigations
Legitimate use of tx.origin — detecting direct EOA calls — the only safe use of tx.origin is to verify that no intermediary contract is involved in the call chain. This is distinct from identity verification:
// Correct: tx.origin used to DETECT intermediaries, not to IDENTIFY the caller
modifier onlyDirectCall() {
require(tx.origin == msg.sender, "Contract calls not permitted");
_;
}
// This modifier ensures the caller is an EOA calling directly — no phishing possible
// because if an intermediary is involved, tx.origin != msg.sender
function sensitiveOperation() external onlyDirectCall {
// Can only be called directly by an EOA, not through any contract
}
Note: this blocks all contract-to-contract calls including legitimate multisigs, DAOs, and protocol integrations. Apply it only where direct EOA interaction is a strict requirement.
OpenZeppelin Ownable — use the battle-tested Ownable implementation which uses msg.sender correctly for all ownership checks:
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureContract is Ownable {
constructor() Ownable(msg.sender) {}
// onlyOwner checks msg.sender == owner() — never tx.origin
function withdraw(address payable recipient, uint256 amount) external onlyOwner {
(bool success,) = recipient.call{value: amount}("");
require(success, "Transfer failed");
}
}
Two-step ownership transfer — for high-value contracts, add a two-step process for ownership changes that requires the new owner to explicitly accept. This prevents transferring ownership to an address that cannot interact with the contract:
import "@openzeppelin/contracts/access/Ownable2Step.sol";
contract TwoStepOwnable is Ownable2Step {
constructor() Ownable(msg.sender) {}
// Transfer: nominates a pending owner
// The pending owner must call acceptOwnership() to complete the transfer
// Prevents accidental ownership loss from typos
}
Role-based access control — for contracts with multiple privileged roles, use a role-based model where each role is independently managed with msg.sender authentication:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract RoleBasedWallet is AccessControl {
bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(WITHDRAW_ROLE, msg.sender);
}
function withdraw(address payable to, uint256 amount) external onlyRole(WITHDRAW_ROLE) {
// onlyRole checks msg.sender — correct and phishing-resistant
(bool success,) = to.call{value: amount}("");
require(success, "Transfer failed");
}
}
Common Mistakes
Using tx.origin as an allowlist — even if tx.origin is checked against a more restrictive allowlist than just the owner, the phishing vector remains. Any time the owner makes a transaction, tx.origin is the owner, and a phishing intermediary can exploit this regardless of the allowlist check.
Thinking tx.origin is safe for read-only functions — view functions with require(tx.origin == owner) are not directly exploitable for fund draining, but they can be used to leak sensitive information about whether the owner is currently in a transaction. Use msg.sender for consistency.
Combining tx.origin and msg.sender checks without understanding the interaction — some contracts use require(tx.origin == msg.sender || msg.sender == owner) thinking this is a stricter check. This is safe against phishing (because the second disjunct is the correct msg.sender check), but the first disjunct is redundant and confusing. Remove it.
Forgetting to audit inherited contracts for tx.origin — if a base contract or library uses tx.origin for authentication, all derived contracts inherit the vulnerability. Search the entire inheritance chain for tx.origin usage before deployment.