Ethernaut Lvl 6 Delegation Walkthrough: How to abuse the delicate delegatecall
This is a in-depth series around Zeppelin team’s smart contract security puzzles. I’ll give you the direct resources and key concepts you’ll need to solve the puzzles 100% on your own.
This levels requires you to claim ownership of the instance you are given.
Delegatecall
Delegate call is a special, low level function call intended to invoke functions from another, often library, contract.
The advantage of delegatecall() is that you can preserve your current, calling contract’s context. This context includes its storage
and its msg.sender
, msg.value
attributes.
Quick Note on Storage: Ethereum stores data in storage “slots”, which are these 32 byte sized slots. Every time you save a variable to storage, it automatically occupies the remaining space in the current slot, or the next slot in sequence.
In the following diagram, Contract A makes a delegatecall to Contract B’s saveX()
function, which ends up mutating Contract A’s storage. Let’s step through this:
First, Contract A invokes the saveX
function via a delegatecall. The delegatecall overrides Contract B’s storage with the storage of the calling contract, akaStorage A
.
Next, thesaveX
function executes. Notice that originally, Contract B stored bar
to storage slot 0
. So when this function now makes a reference to variable bar
, it once again looks in slot 0
.
However, slot 0
is now a reference pointer to foo
. Thus foo
is set to x
. bar
remains out of scope and is untouched.
tl:dr: when contract A makes a delegatecall to Contract B, it allows Contract B to freely mutate its storage A.
Evidently, the security risks happen when developers use delegatecall() in an unsafe storage context, or inherits from a malicious library (more on this in a later level).
From Solidity docs: If storage variables are accessed via a low-level delegatecall, the storage layout of the two contracts must align in order for the called contract to correctly access the storage variables of the calling contract by name. This is of course not the case if storage pointers are passed as function arguments as in the case for the high-level libraries.
Now leverage your deeper understanding of delegatecall() to obtain ownership of this level’s contract!
Detailed Walkthrough
- Notice
Delegation.sol
makes adelegatecall
to the Library contractDelegate.sol
- Notice
Delegate.sol
has a public function called pwn(), which changes the ownership of aowner
variable to whoever invoked the function!
contract Delegate {
address public owner; // Occupies slot 0
... function pwn() public {
owner = msg.sender; // Save msg.sender to slot 0
}
}
3. Notice slot 0
of your Delegation contract also stores the owner
, exactly the variable you wish to change! Furthermore, it seems that if you manage to invoke the fallback function in Delegation.sol
to invoke pwn()
, you’ll become the calling contract’s owner.
function() public {
if(delegate.delegatecall(msg.data)) {
this;
}
}
4. Remember that in Ethereum, you can invoke a public function by sending data
in a transaction. The format is as follows:
contractInstance.call(bytes4(sha3("functionName(inputType)"))
5. Using Remix IDE or the console, invoke Delegation.sol
’s fallback function:
// I did so in the console, having already computed
// the bytes4(sha3("pwn()"))await sendTransaction({
from: "0x1733d5adaccbe8057dba822ea74806361d181654",
to: "0xe3895c413b0035512c029878d1ce4d8702d02320",
data: "0xdd365b8b0000000000000000000000000000000000000000000000000000000000000000"
});
6. await contract.owner()
reveals that you are now the owner!
Tip: you can step through the Remix debugger (while in Javascript VM mode) to see how the storage context changes! You can find the storage slots under Remix debugger’s storage fully loaded
dropdown.
Congrats, you have now reproduced the core vulnerability behind the $30M Parity hack!
Key Security Takeaways
- Use the higher level
call()
function to inherit from libraries, especially when you i) don’t need to change contract storage and ii) do not care about gas control. - When inheriting from a library intending to alter your contract’s storage, make sure to line up your storage slots with the library’s storage slots to avoid these edge cases.
- Authenticate and do conditional checks on functions that invoke delegatecalls.
- Read solidity documentation on security considerations.