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: 0xf1
Introduced: Frontier (EVM genesis)
CALL executes code from another account with specified gas, value, and calldata. The callee runs in its own context with msg.sender set to the caller and msg.value to the transferred amount. This is the primary mechanism for inter-contract communication.
Specification
Stack Input:
gas (max gas to forward)
address (target account to call)
value (wei to send)
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 + value_transfer + new_account + cold_access + memory_expansion
Operation:
calldata = memory[inOffset:inOffset+inLength]
success = external_call(address, value, calldata, gas * 63/64)
memory[outOffset:outOffset+outLength] = returndata[0:min(outLength, returndata.length)]
push(success)
Behavior
CALL performs a nested execution in the target account’s context:
- Pop 7 stack arguments in order: gas, address, value, inOffset, inLength, outOffset, outLength
- Validate static context: Cannot transfer value in static call (EIP-214)
- Calculate gas cost:
- Base: 700 gas (Tangerine Whistle+)
- Value transfer: +9,000 gas if value > 0
- New account: +25,000 gas if recipient doesn’t exist and value > 0
- Cold access: +2,600 gas for first access (Berlin+)
- Memory expansion for both input and output regions
- Read calldata from memory at inOffset:inLength
- Forward gas: Up to 63/64 of remaining gas after charging (EIP-150)
- Execute in callee context:
- msg.sender = caller address
- msg.value = transferred value
- Storage = callee’s storage
- Code = callee’s code
- Transfer value: Move ETH from caller to callee (if value > 0)
- Copy returndata to memory at outOffset (up to min(outLength, returndata.length))
- Set return_data buffer to full returndata
- Push success flag (1 if succeeded, 0 if reverted/failed)
- Refund unused gas from child execution
Key rules:
- Callee stipend: +2,300 gas (free) if value > 0 for receive/fallback execution
- Cannot be called with value in static context (EIP-214)
- Success flag pushed even if call reverts (caller continues execution)
- Returndata accessible via RETURNDATASIZE/RETURNDATACOPY
- Call depth limited to 1,024 (pre-Tangerine Whistle enforcement)
Examples
Basic External Call
import { CALL } from '@tevm/voltaire/evm/system';
import { createFrame } from '@tevm/voltaire/evm/Frame';
import { Address } from '@tevm/voltaire/primitives';
const frame = createFrame({
gasRemaining: 1000000n,
address: Address("0x1234..."),
});
// Calldata: function selector for "transfer(address,uint256)"
const calldata = new Uint8Array([
0xa9, 0x05, 0x9c, 0xbb, // keccak256("transfer(address,uint256)")[:4]
// ... ABI-encoded parameters
]);
// Write calldata to memory
for (let i = 0; i < calldata.length; i++) {
frame.memory.set(i, calldata[i]);
}
// Stack: [gas=100000, address, value=0, inOffset=0, inLength=68, outOffset=0, outLength=32]
frame.stack.push(32n); // outLength
frame.stack.push(0n); // outOffset
frame.stack.push(68n); // inLength
frame.stack.push(0n); // inOffset
frame.stack.push(0n); // value
frame.stack.push(BigInt("0x742d35Cc...")); // address
frame.stack.push(100000n); // gas
const err = CALL(frame);
console.log(err); // null (success)
console.log(frame.stack[0]); // 1n (call succeeded) or 0n (call failed)
console.log(frame.return_data); // Returndata from callee
Value Transfer Call
import { Address } from '@tevm/voltaire/primitives';
// Send 1 ETH to address with empty calldata
const frame = createFrame({
gasRemaining: 1000000n,
address: Address("0x1234..."),
});
// Stack: [gas=50000, address, value=1 ETH, inOffset=0, inLength=0, outOffset=0, outLength=0]
frame.stack.push(0n); // outLength
frame.stack.push(0n); // outOffset
frame.stack.push(0n); // inLength
frame.stack.push(0n); // inOffset
frame.stack.push(1_000_000_000_000_000_000n); // value (1 ETH)
frame.stack.push(BigInt("0x742d35Cc...")); // recipient
frame.stack.push(50000n); // gas
const err = CALL(frame);
// Check success
if (frame.stack[0] === 1n) {
console.log("Transfer succeeded");
} else {
console.log("Transfer failed:", frame.return_data);
}
Safe External Call Pattern
contract Caller {
event CallResult(bool success, bytes returnData);
// Safe external call with error handling
function safeCall(
address target,
uint256 value,
bytes memory data
) external returns (bool success, bytes memory returnData) {
// Limit gas to prevent griefing (retain 1/64 for post-call logic)
uint256 gasToForward = gasleft() * 63 / 64;
assembly {
// CALL(gas, address, value, inOffset, inLength, outOffset, outLength)
success := call(
gasToForward,
target,
value,
add(data, 0x20), // Skip length prefix
mload(data), // Data length
0, // Don't copy to memory yet
0
)
// Copy returndata
let size := returndatasize()
returnData := mload(0x40) // Free memory pointer
mstore(returnData, size) // Store length
returndatacopy(add(returnData, 0x20), 0, size)
mstore(0x40, add(add(returnData, 0x20), size)) // Update free pointer
}
emit CallResult(success, returnData);
return (success, returnData);
}
// Revert if call fails
function strictCall(address target, bytes memory data) external payable {
(bool success, bytes memory returnData) = target.call{value: msg.value}(data);
if (!success) {
// Bubble up revert reason
assembly {
revert(add(returnData, 32), mload(returnData))
}
}
}
}
Token Transfer Example
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
contract TokenCaller {
// Call ERC20.transfer using CALL opcode
function transferTokens(
address token,
address recipient,
uint256 amount
) external returns (bool) {
// ABI encode: transfer(address,uint256)
bytes memory callData = abi.encodeWithSelector(
IERC20.transfer.selector,
recipient,
amount
);
bool success;
bytes memory returnData;
assembly {
// CALL with no value transfer
success := call(
gas(), // Forward all gas
token, // Target contract
0, // No ETH sent
add(callData, 0x20), // Calldata location
mload(callData), // Calldata length
0, // Return data location
0 // Return data length
)
// Copy return data
let size := returndatasize()
returnData := mload(0x40)
mstore(returnData, size)
returndatacopy(add(returnData, 0x20), 0, size)
mstore(0x40, add(add(returnData, 0x20), size))
}
// Decode return value (bool)
require(success && returnData.length >= 32, "Transfer failed");
return abi.decode(returnData, (bool));
}
}
Gas Cost
Total cost: 700 + value_transfer + new_account + cold_access + memory_expansion + forwarded_gas
Base Cost: 700 gas (Tangerine Whistle+)
Pre-Tangerine Whistle: 40 gas
Value Transfer: +9,000 gas
Charged when value > 0:
if (value > 0) {
cost += 9000 // CallValueTransferGas
}
Stipend: Callee receives additional +2,300 gas (free to caller) for receive/fallback execution.
New Account: +25,000 gas
Charged when sending value to non-existent account:
if (value > 0 && !accountExists(address)) {
cost += 25000 // CallNewAccountGas
}
Account exists if: balance > 0 OR code.length > 0 OR nonce > 0
Cold Access: +2,600 gas (Berlin+)
EIP-2929 (Berlin+): First access to address in transaction:
if (firstAccess(address)) {
cost += 2600 // ColdAccountAccess
} else {
cost += 100 // WarmStorageRead
}
Pre-Berlin: No access list costs.
Memory Expansion
Dynamic cost 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+: Caller retains 1/64th, forwards up to 63/64:
remaining_after_charge = gas_remaining - base_cost
max_forwarded = remaining_after_charge - (remaining_after_charge / 64)
actual_forwarded = min(gas_parameter, max_forwarded)
Pre-Tangerine Whistle: Forward all remaining gas.
Example Calculation
// Call external contract with value, cold access, memory expansion
const gasRemaining = 100000n;
const value = 1_000_000_000_000_000_000n; // 1 ETH
const inLength = 68; // Function call with parameters
const outLength = 32; // Return value
// Base cost
let cost = 700n; // Tangerine Whistle+
// Value transfer
cost += 9000n; // CallValueTransferGas
// New account (assume account doesn't exist)
cost += 25000n; // CallNewAccountGas
// Cold access (Berlin+, first access)
cost += 2600n; // ColdAccountAccess
// Memory expansion (assume 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: 37,309 gas
// Gas forwarding (63/64 rule)
const afterCharge = gasRemaining - cost; // 62,691 gas
const maxForward = afterCharge - afterCharge / 64n; // 61,711 gas
// Forward min(gas_param, max_forward)
// Total consumed: 37,309 + gas_actually_used_by_callee
Common Usage
Reentrancy Guard
contract ReentrancyGuard {
uint256 private locked = 1;
modifier nonReentrant() {
require(locked == 1, "Reentrancy");
locked = 2;
_;
locked = 1;
}
// Safe withdrawal with reentrancy protection
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount);
// Update state BEFORE external call
balances[msg.sender] -= amount;
// External call (potential reentrancy point)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Multicall Pattern
contract Multicall {
struct Call {
address target;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
// Execute multiple calls in single transaction
function aggregate(Call[] memory calls) external returns (Result[] memory results) {
results = new Result[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory returnData) = calls[i].target.call(calls[i].callData);
results[i] = Result(success, returnData);
}
}
// Execute multiple calls, revert if any fails
function tryAggregate(Call[] memory calls) external returns (bytes[] memory returnData) {
returnData = new bytes[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory data) = calls[i].target.call(calls[i].callData);
require(success, "Call failed");
returnData[i] = data;
}
}
}
Router Pattern
contract Router {
// Route call to appropriate handler based on selector
function route(address target, bytes memory data) external payable returns (bytes memory) {
require(data.length >= 4, "Invalid calldata");
// Extract function selector
bytes4 selector;
assembly {
selector := mload(add(data, 32))
}
// Validate selector against whitelist
require(isAllowed(selector), "Selector not allowed");
// Forward call
(bool success, bytes memory returnData) = target.call{value: msg.value}(data);
if (!success) {
assembly {
revert(add(returnData, 32), mload(returnData))
}
}
return returnData;
}
function isAllowed(bytes4 selector) internal pure returns (bool) {
// Whitelist logic
return true;
}
}
Security
Reentrancy Attacks
CALL’s primary security risk - external code can re-enter caller:
// VULNERABLE: DAO hack pattern
contract Vulnerable {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
// VULNERABILITY: External call before state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // Too late! Already re-entered
}
}
contract Attacker {
Vulnerable victim;
uint256 public attackCount;
receive() external payable {
// Re-enter during CALL
if (attackCount < 10) {
attackCount++;
victim.withdraw(); // Drain funds
}
}
}
Mitigation: Checks-Effects-Interactions pattern:
// SAFE: State updates before external calls
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
// Update state FIRST
balances[msg.sender] = 0;
// External call LAST
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
Return Value Check
Must check success flag - call can fail silently:
// VULNERABLE: Ignoring return value
function unsafeTransfer(address to, uint256 amount) external {
// Assumes success, but call might fail!
to.call{value: amount}("");
}
// SAFE: Check return value
function safeTransfer(address to, uint256 amount) external {
(bool success, ) = to.call{value: amount}("");
require(success, "Transfer failed");
}
Gas Griefing
Callee controls gas consumption:
// VULNERABLE: Unbounded gas consumption
function dangerousCall(address target, bytes memory data) external {
// Forwards 63/64 of all remaining gas!
target.call(data); // Callee can consume all gas
}
// BETTER: Limit forwarded gas
function limitedCall(address target, bytes memory data) external {
// Limit gas to specific amount
target.call{gas: 100000}(data);
}
Returndata Bomb
Large returndata can cause OOG when copying:
// VULNERABLE: Unbounded returndata copy
function unsafeCopy(address target) external returns (bytes memory) {
(bool success, bytes memory data) = target.call("");
return data; // Might copy gigabytes!
}
// SAFE: Limit returndata size
function safeCopy(address target) external returns (bytes memory) {
(bool success, ) = target.call("");
require(success);
// Manual copy with size limit
uint256 size = min(returndatasize(), 1024);
bytes memory data = new bytes(size);
assembly {
returndatacopy(add(data, 32), 0, size)
}
return data;
}
Value Transfer Validation
Ensure sufficient balance before value transfer:
// VULNERABLE: No balance check
function unsafeSend(address to, uint256 amount) external {
to.call{value: amount}(""); // Reverts if insufficient balance
}
// SAFE: Explicit balance validation
function safeSend(address to, uint256 amount) external {
require(address(this).balance >= amount, "Insufficient balance");
(bool success, ) = to.call{value: amount}("");
require(success, "Transfer failed");
}
Implementation
/**
* CALL opcode (0xf1) - Message call into an account
*/
export function call(frame: FrameType): EvmError | null {
// Pop 7 arguments
const gas = popStack(frame);
const address = popStack(frame);
const value = popStack(frame);
const inOffset = popStack(frame);
const inLength = popStack(frame);
const outOffset = popStack(frame);
const outLength = popStack(frame);
// EIP-214: Cannot transfer value in static context
if (frame.isStatic && value > 0n) {
return { type: "WriteProtection" };
}
// Calculate gas cost
let gasCost = 700n; // Base (Tangerine Whistle+)
// Value transfer
if (value > 0n) {
gasCost += 9000n; // CallValueTransferGas
// Check if account exists
const exists = accountExists(address);
if (!exists) {
gasCost += 25000n; // CallNewAccountGas
}
}
// 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 call
const result = executeCall({
target: address,
value: value,
data: calldata,
gas: forwardedGas
});
// Refund unused gas
frame.gasRemaining += result.gasLeft;
// Copy returndata to memory
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