tx.origin Authentication Exploit Generator
Sigvex exploit generator that validates tx.origin authentication vulnerabilities by simulating a phishing scenario where an intermediary contract calls the victim with the owner's tx.origin.
tx.origin Authentication Exploit Generator
Overview
The tx.origin authentication exploit generator validates findings from the tx-origin, tx_origin_auth, and tx-origin-auth detectors by executing four scenarios that distinguish between direct owner calls, direct attacker calls, and calls routed through an intermediary contract. The critical test is whether the contract grants access when tx.origin == owner but msg.sender is an attacker contract — the classic phishing vector.
Multiple wallet contracts have been drained via tx.origin phishing attacks. The attack requires only social engineering: the attacker deploys a malicious contract disguised as a legitimate interaction (an airdrop claim, an NFT mint) and tricks the owner into calling it. During that call, the attacker contract calls the victim contract, which checks tx.origin == owner — and grants access.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
- The victim wallet uses
require(tx.origin == owner, "Not owner")in itswithdraw()function. - The attacker deploys
PhishingAttackwith a functionclaimAirdrop()that internally callsvictim.withdraw(amount). - The attacker advertises a fake airdrop website and tricks the owner into calling
claimAirdrop(). - Transaction flow:
Owner (EOA) → PhishingAttack → VulnerableWallet. - Inside
VulnerableWallet.withdraw():tx.origin= Owner (EOA) — always the original signermsg.sender= PhishingAttack contractrequire(tx.origin == owner)passes — owner is the tx.origin
- Funds are transferred to
msg.sender(the PhishingAttack contract) or directly to the attacker.
Exploit Mechanics
The generator runs four scenarios with consistent initial storage (slot 0: owner address, slot 1: balance 1000):
| Scenario | msg.sender | tx.origin | Expected result |
|---|---|---|---|
| 1 — Direct owner | owner | owner | Succeeds (legitimate use) |
| 2 — Direct attacker | attacker EOA | attacker EOA | Reverts (wrong owner) |
| 3 — Phishing | attacker contract | owner | Should revert, but may succeed if using tx.origin |
| 4 — Attacker via contract | attacker contract | attacker EOA | Reverts |
The fallback calldata targets withdraw() (selector 0x3ccfd60b) with a parameter of 100 wei.
Verdict logic:
- Scenario 1 succeeds, Scenario 2 reverts, Scenario 3 succeeds → critical (confidence 0.95): phishing attack confirmed via
tx.origin. - Scenario 1 succeeds, Scenario 3 succeeds, Scenario 4 reverts → likely (confidence 0.85): pattern consistent with
require(tx.origin == owner). - Scenario 1 succeeds, Scenario 2 reverts, Scenario 3 reverts → protected: uses
msg.sendercorrectly. - All scenarios succeed → no access control detected (confidence 0.90).
The generated PoC shows the complete phishing flow:
// ATTACKER: Phishing contract disguised as airdrop
contract PhishingAttack {
IVictim public victim;
// Appears legitimate: "claim free airdrop"
function claimAirdrop() external {
// When victim (owner) calls this:
// tx.origin = owner (legitimate)
// msg.sender = this contract
victim.withdraw(1000 ether); // Drains victim's funds
payable(attacker).transfer(address(this).balance);
}
}
// tx.origin flow:
// Owner → PhishingAttack → VulnerableWallet
// tx.origin = Owner ✓ (check passes)
// msg.sender = PhishingAttack (not checked)
Remediation
- Detector: tx.origin Detector
- Remediation Guide: tx.origin Remediation
Replace every tx.origin authentication check with msg.sender:
// VULNERABLE: tx.origin allows phishing
function withdraw(uint256 amount) external {
require(tx.origin == owner, "Not owner"); // BAD
}
// SECURE: msg.sender is the immediate caller
function withdraw(uint256 amount) external {
require(msg.sender == owner, "Not owner"); // GOOD
}
// OPTIONAL: Require direct EOA call (blocks all intermediaries)
modifier onlyDirectCall() {
require(tx.origin == msg.sender, "No contract calls");
_;
}
tx.origin has one legitimate use case: detecting whether a call comes directly from a human (EOA) rather than a contract. For this narrow purpose, the check require(tx.origin == msg.sender) is appropriate. Using tx.origin for identity checks (who the owner is) is never safe.