Delegatecall
Delegatecall allows a contract to execute code from another contract while preserving its context (storage, caller, etc.). However, incorrect usage or misunderstanding of delegatecall can lead to devastating results.
When using delegatecall, it’s crucial to keep two things in mind:
delegatecallpreserves context (storage, caller, etc.).The storage layout must be the same for the contract calling
delegatecalland the contract being called.
Exploiting Delegatecall: An Example
Consider the following contract, HackMe, which uses delegatecall to execute code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Lib {
address public owner;
function pwn() public {
owner = msg.sender;
}
}
contract HackMe {
address public owner;
Lib public lib;
constructor(Lib _lib) {
owner = msg.sender;
lib = Lib(_lib);
}
fallback() external payable {
address(lib).delegatecall(msg.data);
}
}In this contract, it’s not immediately obvious that the owner of HackMe can be changed, since there’s no function inside HackMe to do so. However, an attacker can hijack the contract by exploiting delegatecall.
Here’s how the attack works:
Alice deploys
Lib.Alice deploys
HackMewith the address ofLib.Eve deploys
Attackwith the address ofHackMe.Eve calls
Attack.attack().Attackis now the owner ofHackMe.
In step 4, when Eve calls Attack.attack(), Attack calls the fallback function of HackMe, sending the function selector of pwn(). HackMe forwards the call to Lib using delegatecall. This tells Solidity to call the function pwn() inside Lib, which updates the owner to msg.sender. Because delegatecall runs the code of Lib using the context of HackMe, HackMe’s storage was updated to msg.sender, where msg.sender is the caller of HackMe - in this case, Attack.
A More Sophisticated Exploit
The previous example was relatively straightforward but delegatecall can be exploited in more sophisticated ways. For instance, consider the following contracts:
In this example, the state variables are not defined in the same manner in Lib and HackMe. This means that calling Lib.doSomething() will change the first state variable inside HackMe, which happens to be the address of lib.
The Attack
Here’s how the attack works:
Alice deploys
Lib.Alice deploys
HackMewith the address ofLib.Eve deploys
Attackwith the address ofHackMe.Eve calls
Attack.attack().Attackis now the owner ofHackMe.
In step 4, when Eve calls Attack.attack(), the Attack contract does two things:
First, it calls
HackMe.doSomething()with the address ofAttackcast to auint. This causesHackMeto executeLib.doSomething()viadelegatecall, but because the storage layouts ofLibandHackMeare different, it ends up overwriting thelibaddress inHackMewith the address ofAttack.Second, it calls
HackMe.doSomething()again, but this time with an arbitrary number as input. Because thelibaddress inHackMehas been overwritten with the address ofAttack, this causesHackMeto executeAttack.doSomething()viadelegatecall, which changes theownerofHackMeto themsg.sender(which isAttack).
Preventing Delegatecall Exploits
Preventing delegatecall exploits primarily involves understanding how delegatecall works and being aware of its potential dangers. Always remember that delegatecall preserves the context of the calling contract and that the storage layout must be the same for both the calling contract and the called contract. Be cautious when using delegatecall and always thoroughly test your contracts to ensure they are secure.
Using Stateless Libraries
One of the most effective ways to prevent delegatecall exploits is to use stateless libraries. A stateless library in Solidity is a library that does not have any state variables. It only contains functions.
Here’s why stateless libraries can help prevent delegatecall exploits:
Since stateless libraries do not have any state variables, there’s no risk of a
delegatecallunintentionally overwriting state variables in the calling contract.Functions in stateless libraries are often designed to be pure or view, meaning they do not modify the state and only operate on their input arguments. This makes them safer to use with
delegatecall.
Here’s an example of what a stateless library in Solidity might look like:
In this example, the SafeMath library provides safe versions of the addition and subtraction operations. These functions can be used in other contracts via delegatecall without risking any state variable overwrites because SafeMath does not have any state variables
Last updated