This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.
Overview
Opcode: 0xf4
Introduced: Homestead (EIP-7)
DELEGATECALL executes code from another account while preserving the complete execution context (msg.sender, msg.value, storage). This enables library patterns and upgradeable proxy contracts by allowing code reuse without changing the caller’s state context.
Specification
Stack Input:
gas (max gas to forward)
address (target account code to execute)
inOffset (calldata memory offset)
inLength (calldata size)
outOffset (returndata memory offset)
outLength (returndata size)
Stack Output:
success (1 if call succeeded, 0 if failed)
Gas Cost: 700 + cold_access + memory_expansion
Operation:
calldata = memory[inOffset:inOffset+inLength]
success = execute_preserving_context(address.code, calldata, gas * 63/64)
memory[outOffset:outOffset+outLength] = returndata[0:min(outLength, returndata.length)]
push(success)
Behavior
DELEGATECALL executes foreign code as if it were part of the caller:
- Pop 6 stack arguments (no value parameter)
- Calculate gas cost:
- Base: 700 gas (Tangerine Whistle+)
- Cold access: +2,600 gas for first access (Berlin+)
- Memory expansion for input and output regions
- Read calldata from memory
- Forward gas: Up to 63/64 of remaining gas (EIP-150)
- Execute target’s code preserving context:
- msg.sender = preserved from caller (NOT changed!)
- msg.value = preserved from caller (NOT changed!)
- Storage = caller’s storage (modifications affect caller!)
- Code = target’s code
- address(this) = caller’s address
- Copy returndata to memory
- Set return_data buffer to full returndata
- Push success flag
- Refund unused gas from child execution
Key characteristics:
- Complete context preservation (msg.sender, msg.value unchanged)
- Storage operations affect caller
- No value transfer (preserves existing msg.value)
- Foundation for library and proxy patterns
Examples
Library Pattern
import { DELEGATECALL } from '@tevm/voltaire/evm/system';
import { Address } from '@tevm/voltaire/primitives';
const frame = createFrame({
gasRemaining: 1000000n,
address: Address("0x1234..."),
caller: Address("0x5678..."),
value: 1_000_000_000_000_000_000n // 1 ETH from original call
});
// Call library function
const libraryAddress = Address("0xLibrary...");
const calldata = new Uint8Array([/* function selector + params */]);
// Write calldata to memory
for (let i = 0; i < calldata.length; i++) {
frame.memory.set(i, calldata[i]);
}
// Stack: [gas=100000, address, inOffset=0, inLength, outOffset=0, outLength=32]
frame.stack.push(32n); // outLength
frame.stack.push(0n); // outOffset
frame.stack.push(BigInt(calldata.length)); // inLength
frame.stack.push(0n); // inOffset
frame.stack.push(BigInt(libraryAddress)); // library address
frame.stack.push(100000n); // gas
const err = DELEGATECALL(frame);
// In library code:
// - msg.sender = 0x5678... (preserved!)
// - msg.value = 1 ETH (preserved!)
// - storage modifications affect 0x1234...
// - address(this) = 0x1234...
console.log(frame.stack[0]); // 1n if success
Proxy Pattern
contract Proxy {
address public implementation;
constructor(address _implementation) {
implementation = _implementation;
}
// Fallback forwards all calls to implementation
fallback() external payable {
address impl = implementation;
assembly {
// Copy calldata
calldatacopy(0, 0, calldatasize())
// DELEGATECALL to implementation
let result := delegatecall(
gas(),
impl,
0,
calldatasize(),
0,
0
)
// Copy returndata
returndatacopy(0, 0, returndatasize())
// Return or revert based on result
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
// Upgrade implementation (admin only)
function upgrade(address newImpl) external {
require(msg.sender == admin, "Not admin");
implementation = newImpl;
}
}
contract Implementation {
// Storage layout MUST match Proxy!
address public implementation; // Slot 0
address public admin; // Slot 1
uint256 public value; // Slot 2
function setValue(uint256 _value) external {
// Executes in Proxy's context
// msg.sender = original caller (preserved!)
// storage modifications affect Proxy storage
value = _value;
}
function getValue() external view returns (uint256) {
// Reads from Proxy's storage
return value;
}
}
Library Contract
// Library with reusable logic
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "Overflow");
return c;
}
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
uint256 c = a * b;
require(c / a == b, "Overflow");
return c;
}
}
// Using library via DELEGATECALL
contract Calculator {
using SafeMath for uint256;
function calculate(uint256 a, uint256 b) external pure returns (uint256) {
// Compiler generates DELEGATECALL to SafeMath
return a.add(b).mul(2);
}
}
Upgradeable Storage Pattern
// Eternal storage pattern
contract EternalStorage {
mapping(bytes32 => uint256) uintStorage;
mapping(bytes32 => address) addressStorage;
function getUint(bytes32 key) external view returns (uint256) {
return uintStorage[key];
}
function setUint(bytes32 key, uint256 value) external {
uintStorage[key] = value;
}
}
contract LogicV1 {
EternalStorage public store;
function setValue(uint256 value) external {
// Store via delegatecall-safe pattern
store.setUint(keccak256("myValue"), value);
}
function getValue() external view returns (uint256) {
return store.getUint(keccak256("myValue"));
}
}
contract LogicV2 {
EternalStorage public store;
// Upgraded logic with new features
function setValue(uint256 value) external {
require(value > 0, "Must be positive");
store.setUint(keccak256("myValue"), value * 2);
}
function getValue() external view returns (uint256) {
return store.getUint(keccak256("myValue"));
}
}
Gas Cost
Total cost: 700 + cold_access + memory_expansion + forwarded_gas
Base Cost: 700 gas (Tangerine Whistle+)
Pre-Tangerine Whistle: 40 gas
No Value Transfer Cost
DELEGATECALL never transfers value - preserves msg.value:
// No CallValueTransferGas
// No CallNewAccountGas
Cold Access: +2,600 gas (Berlin+)
EIP-2929 (Berlin+): First access to target address:
if (firstAccess(address)) {
cost += 2600 // ColdAccountAccess
} else {
cost += 100 // WarmStorageRead
}
Memory Expansion
Same as CALL - charges for both input and output regions:
const inEnd = inOffset + inLength;
const outEnd = outOffset + outLength;
const maxEnd = Math.max(inEnd, outEnd);
const words = Math.ceil(maxEnd / 32);
const expansionCost = words ** 2 / 512 + 3 * words;
Gas Forwarding (EIP-150)
Tangerine Whistle+: 63/64 rule:
remaining_after_charge = gas_remaining - base_cost
max_forwarded = remaining_after_charge - (remaining_after_charge / 64)
actual_forwarded = min(gas_parameter, max_forwarded)
Example Calculation
// DELEGATECALL to library with cold access
const gasRemaining = 100000n;
const inLength = 68; // Function call
const outLength = 32; // Return value
// Base cost
let cost = 700n; // Tangerine Whistle+
// No value transfer cost
// Cold access (Berlin+, first access)
cost += 2600n; // ColdAccountAccess
// Memory expansion (clean memory)
const maxEnd = Math.max(68, 32); // 68 bytes
const words = Math.ceil(68 / 32); // 3 words
const memCost = Math.floor(words ** 2 / 512) + 3 * words; // 9 gas
cost += BigInt(memCost);
// Total charged: 3,309 gas
// Gas forwarding
const afterCharge = gasRemaining - cost; // 96,691 gas
const maxForward = afterCharge - afterCharge / 64n; // 95,181 gas
// Total consumed: 3,309 + gas_used_by_library
Common Usage
Minimal Proxy (EIP-1167)
// Clone factory using minimal proxy pattern
contract CloneFactory {
// Minimal proxy bytecode (55 bytes)
function clone(address implementation) external returns (address instance) {
bytes20 targetBytes = bytes20(implementation);
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(ptr, 0x14), targetBytes)
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
instance := create(0, ptr, 0x37)
}
require(instance != address(0), "Clone failed");
}
}
// The minimal proxy forwards all calls via DELEGATECALL:
// PUSH1 0x00 CALLDATASIZE PUSH1 0x00 CALLDATACOPY
// PUSH1 0x00 CALLDATASIZE PUSH1 0x00 PUSH20 <impl> GAS DELEGATECALL
// RETURNDATASIZE PUSH1 0x00 PUSH1 0x00 RETURNDATACOPY
// RETURNDATASIZE PUSH1 0x00 RETURN/REVERT
Diamond Pattern (EIP-2535)
contract Diamond {
mapping(bytes4 => address) public facets;
fallback() external payable {
address facet = facets[msg.sig];
require(facet != address(0), "Function does not exist");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
function addFacet(bytes4 selector, address facet) external {
facets[selector] = facet;
}
}
Library Call Helper
contract LibraryUser {
// Safe library call with error handling
function callLibrary(
address library,
bytes memory data
) internal returns (bool success, bytes memory returnData) {
assembly {
success := delegatecall(
gas(),
library,
add(data, 0x20),
mload(data),
0,
0
)
let size := returndatasize()
returnData := mload(0x40)
mstore(returnData, size)
returndatacopy(add(returnData, 0x20), 0, size)
mstore(0x40, add(add(returnData, 0x20), size))
}
}
}
Security
Storage Collision
Storage layout MUST match between caller and callee:
// VULNERABLE: Storage layout mismatch
contract Proxy {
address public implementation; // Slot 0
address public admin; // Slot 1
}
contract MaliciousImpl {
address public owner; // Slot 0 - COLLIDES with implementation!
uint256 public value; // Slot 1 - COLLIDES with admin!
function exploit() external {
// Overwrites Proxy.implementation!
owner = msg.sender;
}
}
Mitigation: Use consistent storage layout:
// SAFE: Matching storage layout
contract Proxy {
address public implementation; // Slot 0
address public admin; // Slot 1
}
contract Implementation {
address public implementation; // Slot 0 - MATCHES
address public admin; // Slot 1 - MATCHES
uint256 public value; // Slot 2 - New state
}
Uninitialized Proxy
Proxy storage must be initialized before use:
// VULNERABLE: Uninitialized proxy
contract Proxy {
address public implementation;
fallback() external payable {
// implementation is address(0)!
address impl = implementation;
assembly {
delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
}
}
}
// SAFE: Initialize in constructor
contract SafeProxy {
address public implementation;
constructor(address _impl) {
require(_impl != address(0), "Invalid implementation");
implementation = _impl;
}
}
Selfdestruct in Implementation
SELFDESTRUCT in library can destroy proxy:
// VULNERABLE: Library can selfdestruct
contract MaliciousLibrary {
function destroy() external {
selfdestruct(payable(msg.sender));
// Destroys PROXY, not library!
}
}
// MITIGATION: Never SELFDESTRUCT in delegatecalled code
// Or use EIP-6780 (Cancun+) which limits SELFDESTRUCT
Function Selector Collision
Different functions with same selector can cause unexpected behavior:
// VULNERABLE: Selector collision
contract Implementation {
// collides_uint256(uint256) and burn(uint256) have same selector!
function collides_uint256(uint256) external { }
function burn(uint256) external { }
}
// MITIGATION: Check for collisions, use namespaced selectors
Delegate to Untrusted Contract
Only DELEGATECALL to trusted, audited code:
// VULNERABLE: User-controlled delegatecall
contract Vulnerable {
function proxyCall(address target, bytes memory data) external {
// User controls target - CAN EXECUTE ARBITRARY CODE!
target.delegatecall(data);
}
}
// SAFE: Whitelist allowed targets
contract Safe {
mapping(address => bool) public allowed;
function proxyCall(address target, bytes memory data) external {
require(allowed[target], "Target not allowed");
target.delegatecall(data);
}
}
Implementation
/**
* DELEGATECALL opcode (0xf4)
* Execute code preserving msg.sender and msg.value
*/
export function delegatecall(frame: FrameType): EvmError | null {
// Pop 6 arguments (no value)
const gas = popStack(frame);
const address = popStack(frame);
const inOffset = popStack(frame);
const inLength = popStack(frame);
const outOffset = popStack(frame);
const outLength = popStack(frame);
// Calculate gas cost
let gasCost = 700n; // Base (Tangerine Whistle+)
// No value transfer cost
// Cold access cost (Berlin+)
const accessCost = getAccessCost(address);
gasCost += accessCost;
// Memory expansion
const inEnd = inLength > 0 ? inOffset + inLength : 0;
const outEnd = outLength > 0 ? outOffset + outLength : 0;
const maxEnd = Math.max(inEnd, outEnd);
if (maxEnd > 0) {
gasCost += memoryExpansionCost(frame, maxEnd);
updateMemorySize(frame, maxEnd);
}
// Calculate forwarded gas (63/64 rule)
const afterCharge = frame.gasRemaining - gasCost;
const maxForward = afterCharge - afterCharge / 64n;
const forwardedGas = min(gas, maxForward);
// Charge total cost
consumeGas(frame, gasCost + forwardedGas);
// Read calldata
const calldata = readMemory(frame, inOffset, inLength);
// Execute delegatecall (preserve context!)
const result = executeDelegateCall({
codeAddress: address, // Code to execute
storageAddress: frame.address, // Storage to modify
sender: frame.caller, // msg.sender PRESERVED
value: frame.value, // msg.value PRESERVED
data: calldata,
gas: forwardedGas
});
// Refund unused gas
frame.gasRemaining += result.gasLeft;
// Copy returndata
const copySize = min(outLength, result.returnData.length);
writeMemory(frame, outOffset, result.returnData.slice(0, copySize));
// Set return_data buffer
frame.returnData = result.returnData;
// Push success flag
pushStack(frame, result.success ? 1n : 0n);
frame.pc += 1;
return null;
}
References