In this post, we discuss phishing attacks due to tx.origin
.
In the regular phishing of a website, phishing begins with a phony email or another kind of communication intended to entice a victim.
In this case, the communication done appears as if it came from a reputable sender. Similarly, the case of smart contracts which use tx.origin
for authorizing users is vulnerable to phishing attacks, where a malicious contract can trick the contract’s owner into executing a function that only the owner should be able to call.
Let us start with msg.sender
vs tx.origin
, followed by how the exploit works, the possible solution, and finally the conclusion.
Let’s get started!
msg.sender vs tx.origin
To explain the difference between msg.sender
and tx.origin
. Consider the scenario given below.
In this case, a user who has an externally owned address/account (EOA), which is nothing but the regular Ethereum wallet address deploys contract A.
Internally when the user calls function two()
, it internally calls function three()
of another contract B.
In such as scenario, for contract B the msg.sender
will be contract A, however, the tx.origin
will be the address that originally initiated the transaction, in this case, the transaction was initiated by EOA (user).
Thus, the msg.sender
represents the caller of the function. It can be a contract or EOA.
In the above case, it was the contract that called function three()
from function two()
, and tx.origin
always represents the EOA that initiated the actual transaction (tx.origin
can never be a contract because contracts cannot send signed transactions).
Exploit
Now that we are clear with the difference between msg.sender
and tx.origin
.
To explain the exploit consider two Solidity contracts, one is the regular savings bank contract to deposit and withdraw and the other is the attack contract. In a phishing attack, the attacker will make use of the contract to attack the victim.
Create a file called savingsBank.sol
contract SavingsBank { address public owner; constructor(){ owner = msg.sender; } function deposit() public payable { // to receive ether. } function withdrawAll(address _recipient) public { require(tx.origin == owner); (bool sent,) = _recipient.call{value:address(this).balance}(""); require(sent, "Failed to send Ether"); } // Helper function to check the balance of this contract function getBalance() public view returns (uint) { return address(this).balance; } }
Create another file called attack.sol
import "./savingsBank.sol"; contract Attacker { SavingsBank public savingsbank; address attacker; constructor(address _savingsBankAddress, address _attackerAddress) { savingsbank = SavingsBank(_savingsBankAddress); attacker = _attackerAddress; } receive() external payable{ savingsbank.withdrawAll(attacker); } // Helper function to check the balance of this contract function getBalance() public view returns (uint) { return address(this).balance; } }
The two contracts are briefly described below
Contract SavingsBank
:
- A constructor that initializes the owner to the
msg.sender
. In this case, themsg.sender
will be the deployer of this contract. Let’s name him ‘Krish’. - A simple
deposit()
payable function is added that can receive Ether. You can as well implement a more complex deposit function. - The
withdrawAll()
uses thetx.origin
to authorize users. If the address of the owner (in this case, the deployer ‘Krish’) is the same astx.origin
, it will transfer all the funds to the recipient. getBalance()
to get the balance of the contract.
Contract Attacker:
- A constructor that will create an instance of
SavingsBank
contract as we know the deployed address ofSavingsBank
and initialize the attacker address. - Implement a function to receive some Ether and unknown to the user ‘Krish’ makes a call to
withdrawAll()
function of the SavingsBank. getBalance()
to get the balance of the attacker contract.
For the compilation and deployment of contracts refer to the Truffle post here.
How does the exploit occur? Let us see this in steps.
Similar to the other phishing attacks on the internet, the attacker can trick the users into performing trustworthy actions on the vulnerable contracts.
The attacker can disguise his contract as an Ethereum private address or as a multi-signature wallet and can socially engineer the users to perform some kind of a transaction (in the above ask him/her to send some amount of Ether).
If the victim is not very careful, may not realize that there is some code in the receive()
function of the attacker.
As soon as the victim sends some amount of Ether from his wallet (tx.origin
of the SavingsBank
contract), the withdrawAll()
function of SavingsBank
gets executed successfully because (tx.origin
will be equal to owner), transferring all the Ether to the attacker’s address.
Solution
To avoid such a problem, the solution would be to use ‘msg.sender
’ instead of ‘tx.origin
’ for authorizations in the contracts.
In this case, when receive()
calls withdrawAll()
, the msg.sender
will be the address of the attacker and thus preventing it from executing successfully.
Conclusion
This post discussed the phishing vulnerability of contracts using tx.origin
for authorizing actions.
This is not to say that the Solidity variable tx.origin
should never be used as there can be legitimate use cases when tx.origin
may be required.
One such case can be if you don’t want your contract to be called from an external contract. In such a case you can use require(tx.origin == msg.sender)
, because we know that tx.origin
always represents an EOA.
I wish you all the best and, hopefully, this article helped you to prevent this hack! If you want to learn more about Solidity, check out my course: