Smart contract development on Ethereum introduces powerful yet complex features, and one of the most critical mechanisms behind upgradable contracts is the distinction between call and delegatecall. These two low-level functions allow contracts to interact with each other, but they behave very differently under the hood—especially when it comes to context, state storage, and contract upgrade patterns.
In this article, we’ll dive deep into how call and delegatecall work, explore their practical implications through code examples, and explain why delegatecall forms the foundation of modern proxy-based upgradeability patterns.
What Are call and delegatecall?
In Solidity, call and delegatecall are low-level functions used to invoke external contracts. They provide more control than high-level function calls and are essential for advanced patterns like delegation, libraries, and proxy contracts.
While both can execute code from another contract, the key difference lies in which context the called code runs in:
call: Executes the target contract’s code in its own context—using its storage, balance, andmsg.sender.delegatecall: Executes the target contract’s code in the caller’s context—using the caller’s storage, balance, andmsg.sender.
👉 Discover how leading platforms implement secure smart contract interactions.
This subtle but powerful distinction makes delegatecall ideal for upgradable contract architectures.
Practical Example: Observing Context Differences
Let’s illustrate this with a simple example.
pragma solidity 0.8.13;
contract A {
address public b;
constructor(address _b) {
b = _b;
}
function foo() external {
(bool success, bytes memory data) = b.call(abi.encodeWithSignature("foo()"));
require(success, "Transaction failed");
}
}
contract B {
event Log(address sender, address me);
function foo() external {
emit Log(msg.sender, address(this));
}
}Using call
When contract A uses b.call(...), the function foo() executes inside contract B’s context:
msg.senderis the address of contract A.address(this)inside B refers to B itself.
After execution, the emitted event shows:
Log(A_address, B_address)This confirms that the call ran in B’s environment.
Switching to delegatecall
Now modify contract A:
function foo() external {
(bool success, bytes memory data) = b.delegatecall(abi.encodeWithSignature("foo()"));
require(success, "Transaction failed");
}Even though we're executing B’s logic, the context remains A’s:
msg.senderstill refers to whoever called A.address(this)now points to contract A, not B.
The emitted event becomes:
Log(A_caller, A_address)Despite running B’s code, everything happens as if B’s logic were defined inside A.
👉 Learn how real-world dApps leverage secure upgrade mechanisms.
This behavior unlocks a powerful design pattern: separating logic from data.
State Storage: Why Slot Alignment Matters
One of the most important—and often misunderstood—aspects of delegatecall is how it affects state variable storage.
Because delegatecall uses the caller’s storage, any changes made by the called contract affect the caller’s state slots, not its own.
Consider this setup:
contract A {
uint256 public alice;
uint256 public bob;
address public b;
constructor(address _b) { b = _b; }
function foo(uint256 _alice, uint256 _bob) external {
(bool success, ) = b.delegatecall(
abi.encodeWithSignature("foo(uint256,uint256)", _alice, _bob)
);
require(success);
}
}
contract B {
uint256 public alice;
uint256 public bob;
function foo(uint256 _alice, uint256 _bob) external {
alice = _alice;
bob = _bob;
}
}Even though B.foo() assigns values to its own alice and bob, those assignments actually modify A’s storage slots due to delegatecall.
Here’s what happens at the storage level:
| Contract A Slots | ← Affected by → | Contract B Logic |
|---|---|---|
| Slot 0: alice | ← Written via | Slot 0: alice |
| Slot 1: bob | ← Written via | Slot 1: bob |
The mapping is by slot index, not by variable name. So even if you rename variables or change their names across contracts, as long as the declaration order matches, data integrity is preserved.
⚠️ Warning: Mismatched slot layouts between proxy and logic contracts can lead to silent data corruption.
For example, if B declares variables in a different order:
contract B {
uint256 public bob; // Slot 0
uint256 public alice; // Slot 1
}Then calling foo(_alice, _bob) will write _alice to slot 0 (overwriting A’s bob) and _bob to slot 1 (overwriting A’s alice). The result? Data confusion—even if everything appears correct in code.
How delegatecall Enables Contract Upgrades
This precise behavior—running external logic in the caller’s context—is exactly what powers upgradable smart contracts.
The Proxy Pattern
Most upgradable systems use a proxy pattern:
- A proxy contract holds all persistent data (state).
- A logic (implementation) contract contains executable functions.
- The proxy forwards calls to the logic contract using
delegatecall.
When an upgrade is needed:
- Deploy a new logic contract.
- Update the proxy’s pointer to the new address.
- All future calls go through the same proxy, now using updated logic.
Since storage lives in the proxy and execution runs there via delegatecall, data persists across upgrades.
Key Constraints
However, this approach has strict rules:
- Never reorder existing state variables.
- Append only: New variables must come after existing ones.
- Use an upgradeable compiler (like OpenZeppelin’s) or manually manage gaps (
__gap).
Violating these can misalign storage slots and corrupt data during upgrades.
Frequently Asked Questions (FAQ)
Q: Can I use delegatecall to upgrade any contract?
No. Only contracts designed with upgradeability in mind—typically using a proxy pattern—can be safely upgraded. Regular contracts lack separation between logic and storage.
Q: Is delegatecall dangerous?
Yes, if misused. Because it allows foreign code to manipulate your storage, you must fully trust the target contract. Malicious or buggy logic can drain funds or corrupt data.
Q: Does delegatecall preserve msg.sender?
Yes. One major benefit is that msg.sender remains unchanged—it reflects the original user who initiated the transaction through the proxy.
Q: Can I emit events from the logic contract?
Yes. Events are emitted normally and appear to come from the proxy address since execution occurs within its context.
Q: Are there alternatives to delegatecall?
Yes, but less efficient:
call: Copies logic but runs in separate context—data isn’t shared.- Eternal storage patterns: More complex and rarely used today.
Q: How do I prevent unauthorized upgrades?
Use access control! Only authorized addresses (e.g., a governance contract or owner) should be able to change the logic contract pointer.
Final Thoughts
Understanding call vs delegatecall is not just about mastering low-level Ethereum mechanics—it's foundational knowledge for building secure, maintainable decentralized applications.
While call enables standard inter-contract communication, delegatecall enables powerful architectural patterns like upgradable contracts, library reuse, and modular dApp design. But with great power comes great responsibility: incorrect usage can lead to irreversible vulnerabilities.
As blockchain systems grow more complex, developers must prioritize safety, clarity, and long-term maintainability—especially when dealing with persistent on-chain data.
Core Keywords
- delegatecall in Solidity
- call vs delegatecall
- smart contract upgrade pattern
- Ethereum proxy contract
- storage slot alignment
- upgradable smart contracts
- Solidity low-level calls
- contract context sharing
👉 Explore secure development practices used by top blockchain innovators.