Phishing with tx.origin

msg.sender and tx.origin are global variables that provide information about the origin of a transaction. However, they serve different purposes and using them interchangeably can lead to vulnerabilities in your contract.

What are msg.sender and tx.origin?

  • msg.sender is the address of the immediate sender of the message. If a contract A calls contract B, and contract B calls contract C, in contract C, msg.sender will be contract B.

  • tx.origin is the address of the account that originally initiated the transaction. In the above scenario, in contract C, tx.origin will be contract A.

The Vulnerability

A malicious contract can deceive the owner of a contract into calling a function that only the owner should be able to call. This is possible if the contract uses tx.origin to authorize the owner.

Consider the following contracts:

contract Wallet {
    address public owner;

    constructor() payable {
        owner = msg.sender;
    }

    function transfer(address payable _to, uint _amount) public {
        require(tx.origin == owner, "Not owner");

        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }
}

contract Attack {
    address payable public owner;
    Wallet wallet;

    constructor(Wallet _wallet) {
        wallet = Wallet(_wallet);
        owner = payable(msg.sender);
    }

    function attack() public {
        wallet.transfer(owner, address(wallet).balance);
    }
}

In the given contracts, Wallet is a contract that holds Ether and has a transfer function. This function is intended to be called only by the owner of the contract. The owner is set when the contract is deployed (owner = msg.sender in the constructor).

The transfer function uses tx.origin to check if the caller is the owner. This is where the vulnerability lies. tx.origin is the original sender of the transaction, i.e., the address of the account that started the transaction sequence.

Now, consider another contract Attack. This contract is malicious and is designed to steal Ether from the Wallet contract. The Attack contract also has a transfer function. This function calls the transfer function of the Wallet contract.

Here’s the sequence of events:

  1. Alice deploys the Wallet contract and deposits 10 Ether into it.

  2. Eve deploys the Attack contract, passing the address of Alice’s Wallet contract to the constructor.

  3. Eve tricks Alice into calling Attack.attack().

  4. Attack.attack() calls Wallet.transfer(), requesting to transfer all Ether to Eve’s address.

Since Wallet.transfer() checks tx.origin (which is Alice’s address because she started the transaction by calling Attack.attack()), it authorizes the transfer. As a result, all Ether in the Wallet contract is transferred to Eve’s address.

In Ethereum, tx.origin is the original initiator of the transaction. It’s always the address of an externally owned account (EOA), not a contract. This is because only EOAs can initiate transactions on the Ethereum network.

In the scenario described, Alice is tricked into calling Attack.attack(). This means Alice (an EOA) is the one who initiates the transaction. Therefore, tx.origin will be Alice’s address, regardless of the contracts that are called during the execution of the transaction.

The Attack contract is designed to call the Wallet contract within the same transaction. But since tx.origin only considers the original initiator (Alice in this case), it doesn’t change when another contract is called. This is why tx.origin is still Alice’s address when Wallet.transfer() is executed, even though the immediate caller is the Attack contract.

When we say ā€œAlice is trickedā€, it means that Eve somehow convinces Alice to execute a function in the Attack contract. This could happen in various ways, depending on the specifics of the situation. Here are a few possibilities:

  1. Phishing: Eve could create a user interface that looks like a legitimate service and tricks Alice into interacting with it. When Alice interacts with this interface, she’s unknowingly interacting with the Attack contract.

  2. Hidden Transactions: If Alice is using a dApp with multiple interactions, Eve could hide the malicious Attack.attack() function call within a series of legitimate transactions.

  3. Deceptive Contract Names: Eve could name her contract something that sounds safe or similar to a contract Alice trusts. If Alice doesn’t verify the contract’s address or code, she might interact with it thinking it’s the trusted contract.

Remember, these are just examples. Actual attacks can be complex and varied.

Preventative Techniques

To prevent such vulnerabilities, use msg.sender instead of tx.origin for authorization checks. Here’s how you can modify the transfer function:

By using msg.sender, you ensure that the immediate sender of the message is the owner, not just the originator of the transaction. This prevents malicious contracts from tricking the owner into performing actions they didn’t intend to.

Last updated