Ethernaut Delegation Solution: Foundry Walkthrough

Trace a fallback delegatecall and claim Delegation ownership by sending the pwn() selector.

In Ethernaut level 6, Delegation, the goal is to change Delegation.owner. Calling Delegate.pwn() directly changes the wrong contract. We'll use Foundry in the SCH CTF Arena to send the function selector through the fallback and write to the target's storage.

Ethernaut series Level 06 · Delegation

Contract overview

Delegate has one state variable, owner, and a public function that assigns the caller to it:

function pwn() public {
    owner = msg.sender;
}

Delegation also declares owner first, followed by a reference to the Delegate contract. Its constructor records the deployer as owner and stores the delegate address. It has no declared pwn() function. Instead, unmatched calls reach this fallback:

fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    result;
}

The fallback forwards the incoming calldata to Delegate using delegatecall. It does not restrict the caller or check that the forwarded function is safe.

The code changes Delegation's owner

With delegatecall, the called contract supplies code while the calling contract supplies the execution address and storage. The original msg.sender is preserved. This is different from an ordinary external call to Delegate.

Both contracts place their first address variable in storage slot 0. When Delegate's code executes its ownership assignment through the fallback, it writes the player address to slot 0 in Delegation.

Call made by the player Storage receiving the write Result
Ordinary call to Delegate's pwn() Delegate, slot 0 Delegate's owner changes
Call to Delegation's fallback forwarding pwn() Delegation, slot 0 The challenge owner changes

Delegate's own owner remains unchanged. The fallback executes its exposed pwn() code against Delegation's storage.

Send the function selector

A Solidity function selector is the first four bytes of the Keccak-256 hash of its canonical signature. Here the signature is pwn(), with no arguments:

abi.encodeWithSignature("pwn()")

This produces the four-byte payload 0xdd365b8b. Sending it to Delegation reaches the fallback because that contract has no matching function. Delegate then recognizes the selector and executes its pwn() code.

Run the solution in SCH

The Arena supplies delegationInstance and the player identity. Once the checker is unlocked, paste this function into the editor:

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

    (bool success,) = address(delegationInstance).call(
        abi.encodeWithSignature("pwn()")
    );
    require(success, "Call failed");

    vm.stopBroadcast();
}

Use the Delegation instance as the destination. The low-level call is necessary because its typed interface does not declare pwn().

Checking success catches a failure of the outer call, but this target discards the inner delegatecall result. An outer success alone does not establish that the requested storage write happened.

Verify the correct owner

Check answer requires target.owner() == player(). Local tests also confirm that Delegate's owner is unchanged and a direct call cannot solve the level.

Passing local Foundry tests verifying Delegation ownership and the unchanged Delegate owner.
Ownership is checked on Delegation, where the delegated storage write occurs.

Watch Johnny's walkthrough

Ethernaut Delegation solution with JohnnyTime

Watch on YouTube if the embedded player is unavailable.

Control the code that runs in your storage

A delegate address is a source of executable authority over the caller's state. A proxy must use an implementation that is safe for that storage context, with authorization on sensitive functions and compatible storage layouts. Restricting upgrades alone does not fix an already-exposed ownership function.

The Solidity delegatecall documentation describes the preserved caller and storage context. Forwarders should also handle failure explicitly. Ignoring the return flag can conceal failed execution, as this fallback does.

Challenge source: OpenZeppelin Ethernaut Delegation. Review Telephone's caller identities or solve Delegation in the Arena.