Skip to main content
Sigvex

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

  1. The victim wallet uses require(tx.origin == owner, "Not owner") in its withdraw() function.
  2. The attacker deploys PhishingAttack with a function claimAirdrop() that internally calls victim.withdraw(amount).
  3. The attacker advertises a fake airdrop website and tricks the owner into calling claimAirdrop().
  4. Transaction flow: Owner (EOA) → PhishingAttack → VulnerableWallet.
  5. Inside VulnerableWallet.withdraw():
    • tx.origin = Owner (EOA) — always the original signer
    • msg.sender = PhishingAttack contract
    • require(tx.origin == owner) passes — owner is the tx.origin
  6. 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.sender correctly.
  • 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

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.

References