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 updateownerasmsg.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
delegateand initiliazes an instance ofDelegate. Setmsg.senderasowner. -
fallback: special function that makes adelegatecalltoDelegate.
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.
Top comments (0)