Two ways to ask another contract for help
Imagine you run a small shop and you need the books done. Option A: you phone an accounting firm. They do the work in their office, with their files, and mail you back the answer — your filing cabinet is never opened. Option B: you hand a contractor the keys to your office and say, *use my desk, my filing cabinet, do the work right here.* Both finish the job, but who touches whose files could not be more different. That, exactly, is the difference between a CALL and a DELEGATECALL inside the EVM.
In earlier guides of this rung you saw the EVM as a stack machine chewing through opcodes, and you learned where a contract keeps its data — permanent storage, scratch memory, read-only calldata. But contracts almost never live alone. A DeFi router calls a pool, the pool calls a token, the token logs an event. Every one of those hops is a message call, and the single most important question at each hop is: *whose storage does the called code write to?* Get that wrong and you brick a contract or hand it to an attacker.
CALL: run their code in their house
When contract A does a CALL into contract B, the EVM opens a brand-new execution frame. B's code runs against B's storage and B's balance. From inside B, `msg.sender` is A's address — not the human who started the transaction — and `msg.value` is whatever ETH A chose to forward. B is fully itself; A just asked it a question.
// Contract A makes a normal CALL into Target.
// Inside Target: msg.sender == address(A), msg.value == 1 ether,
// and every SSTORE writes Target's OWN storage.
(bool ok, bytes memory ret) = target.call{value: 1 ether}(
abi.encodeWithSignature("deposit(uint256)", 100)
);
require(ok, "deposit call failed"); // <-- MUST check: a failed CALL does NOT auto-revert youNotice the return shape. The CALL opcode pushes a success boolean onto the stack and copies whatever B returned into the return data buffer. Crucially, if B reverts, the EVM does not automatically revert your transaction — you simply get `ok == false`. Silently ignoring that boolean is the classic unchecked external call bug: you think you paid someone, the transfer quietly failed, and your accounting is now a lie.
- A lays out the calldata (selector + arguments) in its memory.
- The CALL opcode takes: gas to forward, target address, value, and the input/output memory regions.
- A new frame is pushed: msg.sender = A, msg.value = the forwarded ETH, and the active storage is B's.
- B executes; it may write its own storage, emit logs, or revert.
- The frame returns a success boolean plus return data; if value was sent, the balance moved from A to B.
DELEGATECALL: borrow their brain, keep your house
DELEGATECALL is the strange, powerful one. The EVM fetches B's code but runs it in A's context: A's storage, A's balance, and — this is the kicker — `msg.sender` and `msg.value` stay exactly as they were in the call into A. It is as if B's bytecode were copy-pasted into A and executed in place. B is no longer a separate actor; B is a brain on loan.
// Logic library: bump() writes to slot 0 of WHOEVER delegatecalls it.
contract Counter {
uint256 public count; // storage slot 0
function bump() external { count += 1; }
}
// Wallet borrows Counter's code but keeps its OWN storage.
contract Wallet {
uint256 public count; // MUST also be slot 0
function bumpViaLib(address lib) external {
// Runs Counter.bump() in Wallet's context:
// Wallet.count goes up; Counter.count NEVER moves.
// msg.sender and msg.value are inherited unchanged.
(bool ok, ) = lib.delegatecall(abi.encodeWithSignature("bump()"));
require(ok, "delegatecall failed");
}
}This single trick is what makes libraries and upgradeable contracts possible. Deploy the logic once; let many other contracts borrow it via DELEGATECALL, each keeping its own state. The deployed library is shared read-only code; the calling contract supplies the data. That is enormous gas and deployment savings — and, as we'll see, an enormous footgun.
The proxy pattern: upgrade code without moving the data
Deployed bytecode is immutable — you cannot patch a contract in place. So how does any serious protocol ship a bug fix? It splits storage from logic. A thin Proxy holds all the state and all the money; a separate Implementation holds only the code. Every incoming call hits the proxy's fallback, which DELEGATECALLs into the current implementation. To upgrade, you just point the proxy at a new implementation address — same storage, same balance, new behaviour. This is the proxy upgrade pattern.
contract Proxy {
// EIP-1967 implementation slot -- a pseudo-random slot, NOT slot 0,
// chosen as keccak256("eip1967.proxy.implementation") - 1:
bytes32 constant SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
function _impl() internal view returns (address a) {
assembly { a := sload(SLOT) }
}
fallback() external payable {
address impl = _impl();
assembly {
calldatacopy(0, 0, calldatasize()) // copy the incoming calldata
let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize()) // copy back whatever logic returned
switch ok
case 0 { revert(0, returndatasize()) } // bubble up the revert reason
default { return(0, returndatasize()) }
}
}
}Real systems pick a flavour. A transparent proxy routes admin calls and user calls differently to avoid selector clashes. A UUPS proxy puts the `upgradeTo` logic in the implementation (cheaper proxy, but you must never ship an implementation that forgets it). A diamond proxy (EIP-2535) routes different function selectors to different implementation 'facets', escaping the 24 KB contract-size limit. All three rest on the very same DELEGATECALL.
When the context is wrong: storage collisions and the Parity freeze
The power is the peril. Because delegated code writes to the caller's slots by slot number, the proxy and the implementation must agree on the storage layout to the byte. If the proxy keeps `implementation` at slot 0 and the logic contract also uses slot 0 for an ordinary variable, the first write into that variable silently overwrites the implementation pointer. That is a storage collision, and it can brick or hijack a contract in one transaction.
// VULNERABLE: proxy keeps the implementation pointer in slot 0 ...
contract BadProxy {
address public implementation; // slot 0
}
// ... but the logic contract ALSO uses slot 0 for a normal variable:
contract Logic {
address public owner; // slot 0 <-- COLLISION
function setOwner(address o) external { owner = o; }
}
// A delegatecall to setOwner(attacker) writes `attacker` into slot 0,
// which IS the proxy's implementation pointer. The proxy now delegatecalls
// into a wrong/empty address on the next call -> bricked, or hijacked.
// FIX: store proxy internals at a namespaced EIP-1967 slot (see prior section),
// so logic variables starting at slot 0 can never overlap them.This is not theoretical. November 2017, the second Parity multisig incident. Hundreds of Parity wallets shared one library contract, reaching it by DELEGATECALL. That library had been left uninitialized, so an account simply called its `initWallet` and became its owner — then called the library's `kill` function, which ran `SELFDESTRUCT` on the library itself. Every wallet that borrowed its brain now delegatecalled into empty code. Roughly 514,000 ETH — about US$150 million at the time, far more today — was frozen forever, with no bug in the wallets themselves. The category names for this are delegatecall injection and the uninitialized-implementation trap.
Picking the right opcode (and the reentrancy door)
A decision you can carry to any contract: use CALL when you want B to act as itself — transfer ETH, or call an external protocol you don't control. Use DELEGATECALL when you want B's code to act as you — libraries and proxies — and only with code you fully trust, because it can rewrite your entire state. Use STATICCALL when you only need to read.
- CALL into B -> context = B's: storage is B's, msg.sender = you, msg.value = forwarded; can send ETH.
- DELEGATECALL into B -> context = yours: storage is YOURS, msg.sender & msg.value inherited; cannot pass new value.
- STATICCALL into B -> context = B's, but read-only: any state change reverts the call.
You now hold the one distinction that everything from OpenZeppelin's proxies to the biggest losses in DeFi history turns on: *whose code, whose storage.* In the final guide of this rung we follow your Solidity all the way down — through the ABI and the 4-byte selector — to the bytecode dispatcher that decides which function a raw call actually runs.