Ethernaut - Lvl 6: Delegation
pacelliv
Posted on June 5, 2023
Requirements: understanding of delegatecall, fallback special function and methods ID.
The challenge 🤼♀️🤼
Claim ownership of Delegation
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
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;
}
}
}
Inspecting the contracts 🔎🔍
Delegate.sol
contains:
-
owner
: address of the account that owns the contract. -
constructor
: receives an address and set it as the owner of the contract at creation time. -
pwn
: public function to updateowner
asmsg.sender
.
Delegation.sol
contains:
-
owner
: address of the account that owns the contract. -
delegate
: instance ofDelegate
, this variable is of typeDelegate
. - constructor: receives the address of
delegate
and initiliazes an instance ofDelegate
. Setmsg.sender
asowner
. -
fallback
: special function that makes adelegatecall
toDelegate
.
Delegatecall and Fallback ☎️▶️
Delegation
does not have a method that updates owner
after this variable has been initiliazed inside the constructor
. But we can see that in Delegate
there is a method to update the owner in that contract.
The fallback
function in Delegation
contains logic to make a delegatecall
to Delegate
with some encoded data.
delegatecall
is a low-level function in Solidity used to make external calls to another contracts. Let's say we call a function in a contract A which delegates the call to a function in a contract B. delegatecall
will load and run the logic of the function in contract B in the context of A, using the storage of contract A and for this to work contract A needs to share the same storage layout as B, otherwise we will end up writing to the incorrect slots messing with the storage of contract A.
Let's review the following example:
contract Delegatecall {
bool public status; // slot 0
uint256 public num; // slot 1
function updateNum(uint256 _num) external {
num = _num;
}
}
contract BadDelegatecall {
uint256 public num; // slot 0
bool public status; // slot 1
// A `delegatecall` would fail because `BadDelegatecall`
// and `Delegate` does not share the same storage layout.
}
contract GoodDelegatecall {
bool public status; // slot 0
uint256 public num; // slot 1
// A `delegatecall` would succeed because `GoodDelegatecall`
// and `Delegate` share the same storage layout.
}
If we make a delegatecall
from BadDelegatecall
to update num
, this would fail because the variable is in different slots. We would end up writing the value of num
to status
in BadDelegatecall
. delegatecall
will succeed in GoodDelegatecall
because in this case num
is in the same slot in both contracts.
delegatecall
also maintains msg.sender
between external calls. Let's review the following simple chain call:
EOA ---> contract A ---> contract B
`msg.sender` in A and B is the EOA.
The fallback
according to the Solidity docs:
The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable.
The hack 🌌
We need to trigger the fallback
so that it makes a delegatecall
to Delegate
to run the logic of pwn
in Delegation
to update owner
with our address.
We just need to figure out how to target the function pwn
.
When smart contracts are compiled into bytecode, the functions are encoded with an method id, called as selector
, that acts as an unique identifier so that the EVM can identify the method to call. The signature of a function consists of its name and the types of its parameters, for example, for function transfer(address _to, uint _amount)
the function signature is transfer(address,uint)
, no spaces are used.
The selector
of pwn
is what we need to send in our payload for the EVM.
Ok, let's exploit this level. Request a new instance.
Compute the selector of pwn
:
selector = web3.eth.abi.encodeFunctionSignature("pwn()")
Send a raw transaction to Delegation
with the selector
as data
:
await sendTransaction({from: player, to: contract.address, data: selector})
After the transaction in mined, check the owner of Delegation
:
await contract.owner() // should return your address
Submit the instance to complete the level.
Conclusion 💯
Delegatecall
is a powerful function in Solidity, it allow us to dynamically load code at runtime from a different address, maintaining the msg.sender
between external calls.
Not properly understanding or usage of delegatecall
can make a contract vulnerable to exploits because its storage is used by a different contract to execute external logic.
We also need to be careful when calling methods in a contract that implement delegatecall
because we will be the msg.sender
in the callee contract authorizing it to execute its logic with our signature.
Further reading 👀
Posted on June 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.