Unboxing tx.origin. Rune Token case
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.
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.
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!