Unboxing tx.origin. Rune Token case

Unboxing tx.origin. Rune Token case
Photo by Ksenia Yakovleva / Unsplash

Today we saw how ThorChain protocol get rekt on multiple levels. First we saw a black-hat attack the protocol and steal $8M.

That wasn't it. Hacker has also left a message

Could have taken ETH, BTC, LYC, BNB, and BEP20s if waited. Wanted to teach lesson minimizing damage. Multiple critical issues. 10% VAR bounty would have prevented this. Disable until audits are complete. Audits are not a nice to have. Do not rush code that controls 9 figures

☝️will be a join article with explanation on how the first attack from last week happened. Stay tuned!

But not long after that we saw this on their discord channel

How was it possible? Let’s dive into the Rune token implementation.

tx.origin nightmare

Solidity has a global variable, tx.origin which contains the address of the account that originally sent the call (or transaction). In old days of Solidity, this was used as a way to make sure only a EOA (Externally Owned Account) can make calls to certain functions. So how does it differ from msg.sender? Let's consider following scenario.

If account A calls contract B, and B calls contract C, in C msg.sender is contract B and tx.origin is account A.

If that helped protect certain function or was used for authentication, why it's not recommended to use anymore?

Two reasons.

Phishing attack

Contracts that authorize users using the tx.origin variable are typically vulnerable to phishing attacks that can trick users into performing authenticated actions on the vulnerable contract.

Let's consider this example taken from solidity docs

Alice deploys following wallet. Only her can call transferTo function.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
    address owner;

    constructor() {
        owner = msg.sender;
    }

    function transferTo(address payable dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

But Bob deployed following code and tricked Alice to send Ether from her wallet to this contract address.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
interface TxUserWallet {
    function transferTo(address payable dest, uint amount) external;
}

contract TxAttackWallet {
    address payable owner;

    constructor() {
        owner = payable(msg.sender);
    }

    receive() external payable {
        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}

When this contract receives Ether, receive() function is automatically run and it tricks Alice to run transferTo from her own wallet contract and steal all Alice Ether.

It does work because of the tx.origin authentication check. tx.origin in this case is Alice's address and msg.sender is Alice's wallet. Bob deployed succesfull phising attack on Alice.

That's well known attack vector but sadly $RUNE was still using tx.origin for approve and call method.

Taken from Rune token implementation

What is even more frustrating is the comment. They were well aware of the issue and still decided to go with this implementation.

Someone took advantage of this and perform phishing camapaing on RUNE holders successfully stealing ~22K of RUNE tokens and dumping them in the market. Attacker went away with ~$70K.

Solution to this would be using ERC20Permit to have approve and transfer done in one call. Check this great article if you want to know more about Permit.

A Long Way To Go: On Gasless Tokens and ERC20-Permit
We have a long way to go for an acceptable UX in the Ethereum ecosystem. One step towards this will be the new ERC20-Permit standard. What is it and how can you use it?

EIP-3074

Another reason tx.origin shouldn't be used is because of the upcoming Shangai hard fork later this year and EIP-3074 that will be included in it.

EIP-3074 introduces two new EVM instructions AUTH and AUTHCALL. The first sets a context variable authorized based on an ECDSA signature. The second sends a call as the authorized. This essentially delegates control of the EOA to a smart contract. This means there will be a way for smart contracts to send transactions in the context of an Externally Owned Account, thus bypassing tx.origin check.

EIP-3074 brings other benefits like possibility of paying for transaction with ERC20 tokens as smart contracts will be able send transactions in the context of an Externally Owned Account. For more info, I recommend reading this blog post.


If you think using tx.origin for authentication is a great idea, think twice and read this article again, and again. I don't recommend using tx.origin anywhere in the code and if your current system is using this global variable, please stop everything and refactor your code. You don't want to end up on the news as next protocol which got rekt.


Thanks for reading, and if you like my writing, you can subscribe to my blog to receive the daily newsletter as I'm currently in the middle of 100 days of blogging challenge. Subscription box below 👇

If the newsletter is not your thing, check out my Twitter @adrianhetman, where I post and share exciting news from the Blockchain world and security.

See you tomorrow!