Cover photo

The Great Impostor: How tx.origin Got Me Robbed by a Friendly-Looking Contract

Fabian Owuor

Fabian Owuor

A cautionary tale of a developer, a cat contract, and a tragic misunderstanding of trust.


Once upon a time in the land of Solidity, I wrote a smart contract. It was bold. It was elegant. It was… secure.

At least, that’s what I told myself.

public owner;

constructor() {
    owner = tx.origin;
}

"Look at that!" I said, proudly flexing in front of my rubber duck. "Only the person who deployed this contract — me — can call the sensitive functions! tx.origin makes sure no impostors get through."

If only I knew.


Enter the Friendly Contract

One day, a new developer friend — let’s call her Mallory — sent me a new dApp to try out. It was called KittySnuggles.sol and promised to send you non-stop pictures of pixelated cats directly to your wallet.

How could I say no?

I clicked.

I approved the transaction.

It did, in fact, show me an adorable cat.

But then… my main contract — the one I lovingly named BankOfMe.sol — sent Mallory all my ETH.

The Heist: How It Happened

Let’s rewind.

My contract had this:

 withdrawAll() external {
    require(tx.origin == owner, "Not the owner!");
    payable(owner).transfer(address(this).balance);
}

Seems fine, right?

WRONG.

Here's what really happened when I clicked on KittySnuggles:

  1. I interacted with KittySnuggles (a malicious contract).

  2. It, in turn, called BankOfMe and triggered withdrawAll().

  3. Inside BankOfMe, tx.origin was still me, because I initiated the transaction!

  4. The contract happily thought I was the one calling it.

  5. But msg.sender was KittySnuggles, not me.

  6. Mallory's code ran. My ETH vanished.

The Root of All Evil: tx.origin

You see, tx.origin is like that overly trusting friend who always believes whoever started the drama is the good guy — even if the drama has passed through a dozen toxic people on its way to you.

Meanwhile, msg.sender is more like a strict bouncer at a nightclub. It checks who is knocking right now, not who told someone to tell someone to knock.

The Fix

Replace:

require(tx.origin == owner, "Not the owner!");

With:

require(msg.sender == owner, "Not the owner!");

Now, only the actual person or contract currently calling the function is checked — not the innocent human who just wanted cat pictures.

My ETH? Gone.

My Lesson? Learned.

Never trust tx.origin for authentication. It's like leaving your keys under the doormat and being surprised when someone uses them.

And Mallory? She’s living her best life somewhere on-chain, sipping piña coladas and rolling in my Ether. And I still don’t have my pixel cats.

Moral of the Story:

“In Solidity, trust the caller (msg.sender), not the sender’s sender’s sender’s sender’s… ghost.”

The Great Impostor: How tx.origin Got Me Robbed by a Friendly-Looking Contract