Solia saysDELEGATECALL runs another contract’s code in the caller’s storage and msg.sender context — the basis of proxies, libraries, and the diamond pattern.
CALL executes the target contract in the target's context: target storage is written, msg.sender becomes the caller. DELEGATECALL executes the target's code in the caller's context: the caller's storage is written, and msg.sender stays whoever called the proxy. This is what makes proxy upgradeability work — a thin proxy contract delegatecalls into a logic contract, but storage lives in the proxy.
The risk is real: if the logic contract has a different storage layout than the proxy, delegatecalls write to the wrong slots and corrupt state. That bug class — proxy storage collisions — has burned more than one protocol. The demo contrasts the two call modes side by side.
Power-ups you unlock
CALL: executes in target context; target storage written
DELEGATECALL: executes target code in CALLER context; caller storage written
msg.sender preserved through delegatecall
Basis of upgradeable proxies, libraries, and EIP-2535 diamonds
Risk: storage layout mismatch silently corrupts state
The Reentrancy Reaper attacks — common mistakes
Delegatecalling into untrusted code — it can rewrite your storage
Upgrading logic with a different storage layout than the proxy
Confusing msg.sender (preserved) with tx.origin (always EOA)
Using CALL where DELEGATECALL was intended in a library pattern
Boss battleShow msg.sender preserved through delegatecall and corrupted state when the target writes to slot 0.
Example code
<!doctype html><html><head><meta charset="utf-8"></head>
<body style="background:#06040d;color:#e6e0ff;font-family:monospace;padding:20px"><pre id="o"></pre>
<script>
// proxy P delegatecalls into logic L; storage lives in P.
const P = { addr:'0xProxy', storage:{ slot0:'PROXY-owner', slot1:42 } };
const L = { addr:'0xLogic', storage:{ slot0:'LOGIC-owner', slot1:99 }, code:'storage[0] = msg.sender' };
const EOA = '0xUser';
function CALL(caller, target, msgSender){
target.storage.slot0 = msgSender;
return { ran_in:target.addr + ' context', msg_sender:msgSender, P:JSON.stringify(P.storage), L:JSON.stringify(L.storage) };
}
function DELEGATECALL(caller, target, originalSender){
caller.storage.slot0 = originalSender; // caller's storage written by target's code
return { ran_in:caller.addr + ' context', msg_sender:originalSender + ' (PRESERVED)', P:JSON.stringify(caller.storage), L:JSON.stringify(L.storage) };
}
const r1 = CALL(EOA, L, EOA);
const r2 = DELEGATECALL(P, L, EOA);
document.getElementById('o').textContent = [
'CALL ' + EOA + ' → L:',
' ran in: ' + r1.ran_in,
' msg.sender:' + r1.msg_sender,
' L storage: ' + r1.L,
' P storage: ' + r1.P + ' (untouched)',
'',
'DELEGATECALL ' + EOA + ' → P → L (code runs in P):',
' ran in: ' + r2.ran_in,
' msg.sender:' + r2.msg_sender,
' P storage: ' + r2.P + ' (slot0 OVERWRITTEN by L code)',
' L storage: ' + r2.L,
'',
'→ proxy delegatecalls let logic upgrade, but storage layout MUST match'
].join('\n');
</script></body></html>