Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
These three opcodes look similar but differ in what context they execute against — and that distinction is the source of nearly every proxy-pattern bug, including the famous Parity multisig freeze. Picking the wrong one can either lock funds permanently or silently let a callee mutate the caller's storage.
Three call variants, three context behaviors.
Use these three in order. Each builds on the one before.
In one paragraph, explain the difference between CALL, DELEGATECALL, and STATICCALL.
Walk me through what 'context' means here — exactly which fields (caller, callvalue, storage, etc.) are preserved or replaced for each variant.
Given a transparent proxy pattern, explain why the implementation contract must NEVER initialize storage in its constructor and what could go wrong if it does.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Callee {
uint256 public x;
function setX(uint256 v) external { x = v; }
}
contract Caller {
uint256 public x;
address public callee;
// CALL: storage writes hit Callee.x
function viaCall(uint256 v) external {
callee.call(abi.encodeWithSignature("setX(uint256)", v));
}
// DELEGATECALL: storage writes hit Caller.x (msg.sender + storage preserved)
function viaDelegate(uint256 v) external {
callee.delegatecall(abi.encodeWithSignature("setX(uint256)", v));
}
// STATICCALL: read-only; setX would revert
function viaStatic() external view returns (bytes memory) {
(, bytes memory data) = callee.staticcall(abi.encodeWithSignature("x()"));
return data;
}
}