Skip to main content

Overview

System instructions enable contracts to interact with external accounts, create new contracts, and manage account lifecycle. These are the most complex EVM opcodes, handling gas forwarding, context preservation, and state modifications.

Instructions

Contract Creation

Message Calls

Account Management

Key Concepts

Gas Forwarding (EIP-150)

63/64 Rule (Tangerine Whistle+): Caller retains 1/64th of remaining gas, forwards up to 63/64:
max_forwarded = remaining_gas - (remaining_gas / 64)
Pre-Tangerine Whistle: Forward all remaining gas.

Call Variants

Opcodemsg.sendermsg.valueStorageState ChangesIntroduced
CALLcallerspecifiedcalleeallowedFrontier
CALLCODEcallerspecifiedcallerallowedFrontier
DELEGATECALLpreservedpreservedcallerallowedHomestead
STATICCALLcaller0calleeforbiddenByzantium

Value Transfer Gas Costs

With non-zero value:
  • Base call cost: 700 gas (Tangerine Whistle+)
  • Value transfer: +9,000 gas
  • Stipend to callee: +2,300 gas (free)
  • New account: +25,000 gas (if recipient doesn’t exist)
Without value:
  • Base call cost: 700 gas (Tangerine Whistle+)
  • Access cost: 100 (warm) or 2,600 (cold) gas (Berlin+)

Address Computation

CREATE:
address = keccak256(rlp([sender, nonce]))[12:]
CREATE2:
address = keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]

Security Considerations

Reentrancy

External calls enable reentrancy attacks:
// VULNERABLE: State change after external call
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);

    // External call before state update
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);

    balances[msg.sender] -= amount;  // Too late!
}
Mitigation: Checks-Effects-Interactions pattern:
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);

    // Update state BEFORE external call
    balances[msg.sender] -= amount;

    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

CREATE2 Address Collision

CREATE2 enables deterministic addresses but introduces collision risks:
// Attack: Deploy different code at same address
// 1. Deploy contract A with init_code_1
// 2. SELFDESTRUCT contract A (pre-Cancun)
// 3. Deploy contract B with init_code_2 using same salt
// Result: Different code at same address!
EIP-6780 (Cancun) mitigation: SELFDESTRUCT only deletes if created in same transaction.

CALLCODE Deprecation

CALLCODE is deprecated - use DELEGATECALL instead:
// ❌ CALLCODE: msg.value preserved but semantics confusing
assembly {
    callcode(gas(), target, value, in, insize, out, outsize)
}

// ✅ DELEGATECALL: Clear semantics, preserves full context
assembly {
    delegatecall(gas(), target, in, insize, out, outsize)
}

SELFDESTRUCT Changes (EIP-6780)

Pre-Cancun: SELFDESTRUCT deletes code, storage, nonce. Cancun+: SELFDESTRUCT only transfers balance (unless created same tx).
// Pre-Cancun: Code/storage deleted at end of tx
selfdestruct(beneficiary);
// Contract unusable after tx

// Cancun+: Code/storage persist
selfdestruct(beneficiary);
// Contract still functional after tx!

Gas Cost Summary

Call Instructions

OperationBaseValue TransferNew AccountCold AccessMemory
CALL700+9,000+25,000+2,600dynamic
CALLCODE700+9,000-+2,600dynamic
DELEGATECALL700--+2,600dynamic
STATICCALL700--+2,600dynamic

Creation Instructions

OperationBaseInit CodeMemoryNotes
CREATE32,000+6/byte (EIP-3860)dynamic+200/byte codesize
CREATE232,000+6/byte + 6/byte (hashing)dynamic+200/byte codesize

SELFDESTRUCT

  • Base: 5,000 gas
  • Cold beneficiary: +2,600 gas (Berlin+)
  • New account: +25,000 gas (if beneficiary doesn’t exist)
  • Refund: 24,000 gas (removed in London)

Common Patterns

Safe External Call

function safeCall(address target, bytes memory data) internal returns (bool) {
    // 1. Update state first (reentrancy protection)
    // 2. Limit gas forwarded
    // 3. Handle return data safely

    (bool success, bytes memory returnData) = target.call{
        gas: 100000  // Limit forwarded gas
    }(data);

    if (!success) {
        // Handle error (revert or return false)
        if (returnData.length > 0) {
            assembly {
                revert(add(returnData, 32), mload(returnData))
            }
        }
        return false;
    }

    return true;
}

Factory Pattern (CREATE2)

contract Factory {
    function deploy(bytes32 salt) external returns (address) {
        bytes memory bytecode = type(Contract).creationCode;

        address addr;
        assembly {
            addr := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        require(addr != address(0), "Deploy failed");

        return addr;
    }

    function predictAddress(bytes32 salt) external view returns (address) {
        bytes32 hash = keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(type(Contract).creationCode)
        ));
        return address(uint160(uint256(hash)));
    }
}

Proxy Pattern (DELEGATECALL)

contract Proxy {
    address public 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 return data
            returndatacopy(0, 0, returndatasize())

            // Return or revert
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

References