Overview
This level introduces us to the following concept(s):
- The
delegatecall
low-level function in Solidity
GitHub Repository available at: github.com/0xEval/ethernaut-x-foundry
Objective
To complete the challenge, we will need to:
- Claim ownership of the instance
Contract Analysis
When the instance is initialized, two contracts will be deployed Delegate
and Delegation
.
A Delegate
is simply identified by a public address owner
and has a function pwn()
which will change ownership to the sender.
A Delegation
contains both an owner
and a reference to a Delegate
. It has a fallback()
function which uses the delegatecall()
low-level function on the delegate instance, forwarding msg.data
.
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;
}
}
}
Understanding the Delegatecall() function
๐ Quick reminder on the call()
function: solidity-by-example.org/call
There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.
This tells us the main different between call()
and delegatecall()
is data/execution context.
Let's draft a simple example:
// B is deployed at address 0x0B...
contract B {
uint public value;
address public sender;
function set(uint256 _value) public {
value = _value;
}
}
// A is deployed at address 0x0A...
contract A {
uint public value;
address public sender;
function set(uint256 _value, address _addr) public {
_addr.delegatecall(
abi.encodeWithSignature("set(uint256)", _value)
);
}
}
A.set(10, 0x0B)
will trigger delegatecall()
which in turn will execute the set()
function, as defined in B's code but using A's storage
A.value = 10
A.sender = 0x0A
B.value = 0
B.sender = 0
This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.
In fact, this pattern allows the implementation of libraries (such as SafeMath
) in Solidity!
๐ก Recommended reads:
Knowing this should be enough for us to hack the contract, let's see how.
Attacking The Contract
Make sure to read through Part 0 for setup instructions.
Create the Delegation.t.sol
test file in the test/
directory that will contain the attack logic (level setup and submission is truncated for clarity):
We send a call to
Delegation
passing the signature ofDelegate.pwn()
. Since the function is not present in Delegation's code, thefallback
method is triggered.This in turn will execute the low-level
delegatecall()
function and pass the signature in parameter as part ofmsg.data
Since it is a delegated call, the executed code will be
Delegate.pwn()
but in context ofDelegation
's storage.
After the call, the owner
of Delegation should set to our attacker EoA.
function testDelegationHack() public {
address(delegationContract).call(abi.encodeWithSignature("pwn()"));
assertEq(delegationContract.owner(), attacker);
}
Run the attack using the forge test
subcommand: