---
id: 28
title: "CALL 계열 대조: CALL, CALLCODE, DELEGATECALL, STATICCALL"
slug: 0-17-call-family
date: 2026/10/04
summary: 네 가지 호출 명령어는 바이트코드에서 번호 몇 개만 다르지만, 그 의미는 코드가 누구의 컨텍스트에서 실행되고 누구의 스토리지에 쓰며 msg.sender와 address(this)가 무엇이 되는지를 결정합니다. CALL, CALLCODE, DELEGATECALL, STATICCALL을 하나씩 대조하면 프록시가 왜 DELEGATECALL에 의존하는지, STATICCALL이 무엇 때문에 도입되었는지가 분명해집니다.
keywords: CALL,DELEGATECALL,STATICCALL,CALLCODE,프록시 컨트랙트
heroImage: /images/articles/photos/0-17-call-family.jpg
---

평범한 트랜잭션 하나가 컨트랙트 A로 들어가고, A가 DELEGATECALL로 실행을 구현 컨트랙트 B에 넘깁니다. 실행되는 바이트코드는 B에서 오지만 `address(this)`가 반환하는 것은 A의 주소이고, `msg.sender`는 트랜잭션을 처음 시작한 계정이며, B에서 storage에 하는 쓰기는 A의 슬롯에 떨어집니다. DELEGATECALL을 CALL로 바꾸면 이 값들이 전부 달라지고, STATICCALL로 바꾸면 어떤 쓰기 연산이든 곧바로 예외를 던집니다.

네 가지 호출 명령어는 바이트코드 수준에서는 번호 하나만 다르지만, 의미 차이는 프록시 업그레이드와 라이브러리 호출, 읽기 전용 조회, 재진입 방어를 관통합니다. 이 글은 이들이 실행 컨텍스트, 스토리지 소유, 메시지 필드, gas에서 보이는 동작을 하나씩 대조하며, DELEGATECALL이 왜 프록시 컨트랙트의 기반이 되었는지, STATICCALL은 왜 EIP 하나를 따로 들여와야 했는지, CALLCODE는 또 왜 Frontier의 구성원에서 폐기된 유산으로 바뀌었는지 설명합니다.

## 네 명령어의 번호와 스택 매개변수

먼저 겉모습을 맞춰 봅니다. CALL은 `0xf1`, CALLCODE는 `0xf2`, DELEGATECALL은 `0xf4`, STATICCALL은 `0xfa`입니다. 네 명령어는 실패하면 스택에 0을, 성공하면 1을 밀어 넣고, 모두 메모리 오프셋과 길이로 입출력 구간을 서술하며, 모두 1024단계 호출 깊이 제한을 받고, gas가 부족하면 조용히 잘리는 대신 실패합니다.

스택 매개변수 개수가 이들을 두 묶음으로 나눕니다. CALL과 CALLCODE는 각각 피연산자 7개를 취합니다. `gas`, 대상 주소, `value`, 입력 오프셋, 입력 길이, 출력 오프셋, 출력 길이입니다. DELEGATECALL과 STATICCALL은 각각 6개를 취하며 `value` 항목이 없습니다. DELEGATECALL은 부모 스코프의 `msg.value`를 그대로 쓰고, STATICCALL은 그것을 0으로 고정합니다. 매개변수 표의 차이 자체가 의미를 이해하는 입구입니다. `value`를 스스로 정할 수 있는 두 명령어 가운데 하나는 실제로 송금하고(CALL), 다른 하나는 송금하지 않습니다(CALLCODE). `value`를 스스로 정할 수 없는 두 명령어 가운데 하나는 상속하고(DELEGATECALL), 다른 하나는 0으로 지웁니다(STATICCALL).

반환 데이터를 처리하는 방식도 대조해 볼 만합니다. 네 명령어는 모두 자식 호출의 반환 데이터를 호출자가 지정한 메모리 구간에 쓰지만, 쓰는 길이는 호출자가 준 `out_size`로 제한됩니다. 비잔티움 업그레이드가 `RETURNDATASIZE`와 `RETURNDATACOPY`(EIP-211)를 도입한 뒤로는 호출자가 반환 데이터 크기를 미리 짐작할 필요 없이 먼저 길이를 읽고 필요할 때 복사할 수 있게 되었고, 그래서 프록시와 범용 포워딩 컨트랙트는 충분히 큰 출력 영역을 미리 확보해 둘 필요가 없어졌습니다.

## 하나씩 대조: 코드는 어디서 오고, 상태는 어디에 쓰이며, 메시지 필드는 누구의 것인가

CALL은 새 실행 컨텍스트를 만듭니다. 코드는 대상 주소에서 가져오고 storage도 대상 주소에 속하며, `address(this)`는 대상 컨트랙트이고 `msg.sender`는 호출을 시작한 현재 컨트랙트입니다. `value`로 지정한 이더는 현재 컨트랙트에서 대상 주소로 실제 이전되므로 현재 컨트랙트 잔액이 충분해야 하고, 그렇지 않으면 호출이 실패합니다.

CALLCODE도 대상 주소의 코드를 가져와 실행하지만, 실행은 현재 계정의 컨텍스트에서 일어납니다. storage는 현재 컨트랙트의 것이고 `address(this)`는 현재 컨트랙트의 주소입니다. CALL보다 직관에 어긋나는 점이 하나 더 있습니다. `value`가 호출자가 지정한 임의의 값일 수 있어 `msg.value`가 부모 스코프와 다른 숫자로 바뀔 수 있는데, 이른바 가치 이전은 현재 계정이 자기 자신에게 보내는 것이라 실제 잔액 변화를 만들지 않습니다. 가치 검사는 여전히 필요합니다. 옐로 페이퍼의 `0xf2` 정의는 `value`가 현재 계정 잔액을 넘지 않을 것을 호출 전제로 요구하며, 그렇지 않으면 호출 대상 코드로 들어가지 않고 스택에 0을 얻습니다. CALLCODE의 `msg.sender`는 그것을 실행하는 현재 컨트랙트이며, 이 점은 CALL과 같고 DELEGATECALL과 가장 결정적으로 갈리는 지점이기도 합니다.

DELEGATECALL도 현재 계정의 컨텍스트에서 대상 코드를 실행하고 storage와 `address(this)`가 모두 현재 컨트랙트를 가리키지만, 부모 스코프의 `msg.sender`와 `msg.value`를 그대로 자식 스코프로 가져갑니다. EIP-7의 표현은 이렇습니다. 발신자와 가치는 부모 스코프에서 자식 스코프로 전파되며, 자식 코드에서 `CALLER`와 `VALUE`의 동작은 부모 환경과 완전히 같습니다. 여기서 직접적인 이점이 나옵니다. 구현 컨트랙트가 `msg.sender`와 `msg.value`를 자유롭게 참조할 수 있고, 프록시 호출과의 호환을 위해 따로 재컴파일할 필요가 없습니다. EIP-7의 동기 부분은 용도도 두 가지 더 나열합니다. 구현 코드를 여러 조각으로 나눠 단계별로 실행해 당시 약 300만 gas의 호출 상한을 우회하는 것, 그리고 변경 가능한 주소 하나에 코드 출처를 저장해 호출을 그 주소로 그대로 전달하는 것입니다.

STATICCALL이 만드는 실행 컨텍스트는 CALL과 비슷합니다. 코드와 storage가 모두 대상 주소에 속하고, `address(this)`는 대상 컨트랙트이며, `msg.sender`는 호출자이고 `msg.value`는 0입니다. 차이는 자식 스코프에 정적 표시를 붙여 실행 중 어떤 상태 수정도 금지한다는 점입니다. EIP-214가 나열한 금지 항목에는 `CREATE`, `CREATE2`, `LOG0`부터 `LOG4`, `SSTORE`, `SELFDESTRUCT`, 그리고 0이 아닌 `value`를 실은 `CALL`이 포함됩니다. 이런 연산을 만나면 수정을 수행하는 대신 곧바로 예외를 던집니다. 명세는 예외를 하나 남겨 두었습니다. `CALLCODE`는 0이 아닌 `value`를 실어도 상태 수정으로 치지 않는데, CALLCODE의 의미상 그 가치가 현재 계정을 떠나지 않기 때문입니다.

## 대조표 하나

아래 표의 메시지 필드는 호출 대상 코드 내부에서 관측되는 값을 뜻하고, `address(this)`는 대상 코드 실행 시 `ADDRESS`가 스택에 밀어 넣은 결과를 뜻합니다.

| 명령어 | 번호 | 스택 매개변수 | 코드 출처 | storage 소유 | address(this) | msg.sender | msg.value | 상태 쓰기 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| CALL | 0xf1 | 7(value 포함) | 대상 주소 | 대상 주소 | 대상 주소 | 현재 컨트랙트 | 지정 값, 실제 송금 | 허용 |
| CALLCODE | 0xf2 | 7(value 포함) | 대상 주소 | 현재 컨트랙트 | 현재 컨트랙트 | 현재 컨트랙트 | 지정 값, 잔액 검사를 통과해야 하지만 실제 송금은 없음 | 허용 |
| DELEGATECALL | 0xf4 | 6 | 대상 주소 | 현재 컨트랙트 | 현재 컨트랙트 | 부모 스코프 상속 | 부모 스코프 상속 | 허용 |
| STATICCALL | 0xfa | 6 | 대상 주소 | 대상 주소 | 대상 주소 | 현재 컨트랙트 | 0으로 고정 | 금지 |

## Gas: 기본 가격, 접근 비용과 63/64 보존 규칙

네 명령어의 gas는 여러 부분이 겹쳐서 정해집니다. 기본 비용, 대상 주소 접근 비용, 메모리 확장 비용, 그리고 실제로 자식 호출에 넘기는 한도입니다. 자식 호출에 넘긴 부분은 다 쓰이지 않으면 돌아오므로, 그것은 지출이라기보다 한도에 가깝습니다.

대상 주소 접근 비용은 베를린 업그레이드(EIP-2929) 이후 냉/온 가격으로 바뀌었습니다. 같은 트랜잭션에서 처음 접근하면 콜드 접근으로 2600 gas를 받고, 다시 접근하면 웜 접근으로 100 gas를 받습니다. 그전에 EIP-150은 CALL, CALLCODE, DELEGATECALL의 기본 비용을 700 gas로 통일했는데, 베를린 이후 호출 계열은 냉/온 가격으로 바뀌어 2600과 100이 고정된 700을 대체했고, 가격 계산은 사용 가능한 gas를 계산하기 전에 일어납니다. 주소 집합은 같은 트랜잭션 안에서 공유되며, 어느 층의 실행이 롤백되면 그 층에서 새로 추가된 접근 기록도 함께 롤백됩니다. 이 비용을 아끼려는 호출자는 트랜잭션에 EIP-2930의 액세스 리스트(access list)를 첨부해 건드릴 주소와 슬롯을 미리 선언할 수 있고, 대가는 항목마다 고정 수수료를 내는 것입니다.

가치 이전과 계정 생성에는 추가 요금이 붙습니다. CALL이 0이 아닌 `value`를 실으면 9000 gas를 더 받고, 수신자가 dead 계정(존재하지 않거나 비어 있음)이라 상태 트리에 새로 만들어야 하면 25000을 더 받습니다. CALLCODE도 0이 아닌 `value`를 실으면 마찬가지로 9000을 더 받지만, 그 가치는 현재 계정으로 가고 계정 생성은 일어나지 않습니다. DELEGATECALL과 STATICCALL에는 `value` 매개변수가 없으므로 이 두 항목도 없습니다.

2300 gas의 stipend는 또 하나 외우기 쉬운 함정입니다. 0이 아닌 `value`를 실은 CALL과 CALLCODE는 자식 호출이 2300 gas를 추가로 받게 합니다. 옐로 페이퍼 수수료 표는 이 한도를 "`G_callvalue`에서 빼는 것"으로 적는데, 즉 9000의 가치 이전 부가 요금 안에 포함되는 것이지 부가 요금과 별도로 지원되는 것이 아닙니다. 이것은 `transfer()`와 `send()`의 gas 상한이 나온 출처이기도 합니다. 이 두 메서드는 2300 gas만 전달하므로, 수신자가 storage에 쓰거나 로그를 남겨야 하면 곧바로 실패합니다. Solidity 문서는 이미 `send()`와 `transfer()`를 권장하지 않음으로 표시하고 제거할 계획이며, CALL로 바꿔 반환값을 직접 확인하라고 권합니다.

EIP-150은 "1/64 보존" 규칙도 도입했습니다. 전달을 요청한 gas가 호출자의 남은 사용 가능 gas의 63/64를 넘으면 실제로는 63/64만 전달하고, 부모 스코프는 실패 반환을 처리할 gas를 언제나 남겨 둡니다. 이 규칙은 초기의 호출 깊이 기반 공격면을 gas 기반 제한으로 바꿔 놓았고, 자식 호출이 gas를 모두 소진해도 부모 호출이 실패 플래그를 받아 계속 실행할 수 있는 이유, 즉 트랜잭션 전체가 함께 소진되지 않는 이유를 설명합니다.

## DELEGATECALL은 프록시 컨트랙트의 기반이고, 대가는 storage layout 제약이다

DELEGATECALL이 있어야 프록시 패턴이 성립합니다. 프록시 컨트랙트가 상태와 자산을 보유하고 호출을 구현 컨트랙트로 전달하며, 구현은 프록시의 컨텍스트에서 프록시의 storage를 읽고 쓰고, 업그레이드할 때는 구현 주소만 바꾸면 상태는 제자리에 남습니다. 이것이 EVM에서 업그레이드 가능한 컨트랙트의 표준 방식이기도 합니다. 최소한의 포워딩 함수는 대략 다음과 같이 씁니다. 구현 주소를 매개변수로 받고, 구현 컨트랙트의 레이아웃과 충돌하지 않도록 storage를 일부러 차지하지 않습니다.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Forwarder {
    function _delegate(address impl) external payable {
        assembly {
            calldatacopy(0, 0, calldatasize())
            // 호출자(프록시)의 컨텍스트에서 impl 코드를 실행하며 storage와 msg.sender가 모두 프록시 쪽에 남음
            let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch ok
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}
```

대가는 저장 레이아웃(storage layout)이 반드시 맞아야 한다는 것입니다. DELEGATECALL은 슬롯 이름을 바꾸지도 옮기지도 않으며, 코드는 슬롯 번호나 컴파일러가 배정한 오프셋으로 읽고 씁니다. 프록시가 스스로 변수를 선언했는데 구현 컨트랙트가 마침 같은 슬롯에 변수를 선언하면 둘이 서로를 덮어씁니다. EIP-1967은 그래서 구현 주소를 `bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)`로 계산한 슬롯, 즉 `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`에 씁니다. 이 위치는 확률적으로 컴파일러가 업무 변수에 배정하지 않는 자리입니다. 마찬가지로 구현 컨트랙트가 상태 변수를 새로 추가할 때는 끝에 덧붙일 수만 있고, 변수를 삭제하거나 순서를 바꾸면 업그레이드 후의 읽기/쓰기가 통째로 어긋납니다.

생성자(constructor)에도 프록시 패턴 특유의 제약이 붙습니다. 구현 컨트랙트를 배포할 때 생성자는 자기 컨텍스트에서 실행되고 구현 컨트랙트 자신의 storage에 쓰며, 프록시의 상태에는 쓰지 않습니다. 따라서 프록시의 초기화는 명시적인 초기화 함수로 수행해야 하고, 한 번만 실행된다는 표시가 있어야 합니다. 그렇지 않으면 누구든 재초기화해 제어권을 가져갈 수 있습니다.

또 한 갈래의 위험은 호출 대상 코드 자체에서 옵니다. DELEGATECALL은 대상 코드에 프록시의 모든 권한을 쥐여 줍니다. 임의의 슬롯에 쓰고, 잔액을 빼내고, 다른 컨트랙트를 호출하고, 심지어 `selfdestruct`로 프록시의 실행 가능성에까지 영향을 줄 수 있습니다. 구현 주소를 마음대로 바꿀 수 있거나 감사받지 않은 라이브러리를 끌어들였다면, 프록시의 제어권을 통째로 넘긴 것과 같습니다. Solidity 라이브러리(library)의 공개 함수가 바로 DELEGATECALL로 호출되므로, 라이브러리 코드는 자체 상태 변수를 선언할 수 없습니다. 그렇지 않으면 호출자의 레이아웃과 충돌합니다. 라이브러리의 `view`도 `pure`도 아닌 함수는 DELEGATECALL로만 호출되도록 설계되어 있고, 그 런타임 코드에는 호출 보호가 붙어 있어 라이브러리 주소로 직접 CALL을 보내면 실행 중 revert됩니다(`view` 또는 `pure` 함수를 호출할 때는 이 보호를 받지 않습니다).

## STATICCALL은 상태 수정을 예외로 만든다

STATICCALL은 EIP-214로 도입되어 비잔티움(Byzantium) 업그레이드에서 활성화되었습니다. EIP-214는 오프코드와 의미만 제시하고 포크 이름은 적지 않았으며, 포크 귀속은 EIP-214를 비잔티움 포함 목록에 넣은 EIP-609에서 나옵니다. 동기는 이렇습니다. 일반 CALL 뒤에는 호출자가 호출 대상 컨트랙트의 상태가 변하지 않았다고 가정할 수 없어, 재진입류 문제를 국소적으로 추론하기가 매우 어렵습니다. EIP-214가 제시한 목표는 정적 호출 전후로 모든 계정의 상태가 일치하는 것이며, 이 호출은 출력만 반환하고 부작용을 내지 않는 순수 함수로 볼 수 있습니다.

구현 방식은 EVM에 `STATIC` 플래그를 하나 추가하는 것입니다. 이 플래그는 기본값이 false이고, 자식 호출로 들어갈 때 보통 그대로 복사되며, STATICCALL만 그것을 true로 설정하고 반환한 뒤 되돌립니다. 금지 항목은 앞 절에서 이미 나열했습니다. 핵심은 이 플래그가 호출 체인을 따라 전파된다는 점입니다. 정적 호출 안에서 다시 CALL을 보내도 자식 호출은 마찬가지로 정적 모드에 놓이므로, 층을 하나 더 우회하는 방식으로 제한을 피할 수 없습니다.

공학적으로 두 갈래 이득이 있습니다. 읽기 전용 조회를 안전하게 조합할 수 있어, 가격 뷰와 다중 컨트랙트 집계 호출이 호출 상대의 상태 변경을 걱정할 필요가 없습니다. 재진입 방어도 더 앞에서 닫을 수 있어, 외부 읽기 전용 호출을 정적 모드에 넣으면 재진입 쓰기가 EVM 수준에서 곧바로 실패합니다. Solidity 0.5.0부터 라이브러리가 아닌 `view`와 `pure` 함수 호출은 기본적으로 STATICCALL로 컴파일되어, 컴파일 시점 제약 위에 런타임 안전망이 한 겹 더해집니다. 라이브러리의 `view` 함수는 예외로, 여전히 DELEGATECALL을 씁니다. EVM에 정적 의미와 위임 의미를 동시에 갖춘 명령어가 없기 때문에 라이브러리의 읽기 전용 함수에는 런타임 상태 보호가 없고, 이 점은 감사할 때 따로 확인해야 합니다.

경계도 분명히 해 둬야 합니다. STATICCALL은 상태 수정만 금지할 뿐 읽기도 콜백도 금지하지 않으며, 재진입의 쓰기 경로를 막았을 뿐 재진입 문제가 통째로 사라진 것은 아닙니다. 또한 `view` / `pure`의 컴파일 시점 검사를 대신할 수 없고, 둘이 덮는 실패 모드도 다릅니다. 정적 호출도 gas를 소모하며, 대상 컨트랙트가 일부러 revert해서 호출자의 수수료를 공격 비용으로 바꿔 버릴 수도 있습니다. 정적 모드는 상태가 변하지 않는다는 것만 보장할 뿐 호출 성공이나 결과의 신뢰성은 보장하지 않습니다.

## CALLCODE의 역사적 위치와 퇴장

CALLCODE는 Frontier 버전부터 존재한 명령어이고, EIP-7의 동기 부분은 그것을 DELEGATECALL의 대조물로 바로 씁니다. 설계 목표는 DELEGATECALL과 비슷해서 둘 다 "남의 코드를 빌려 현재 계정에서 실행하는" 것이지만, 메시지 필드 처리가 다릅니다. `msg.sender`가 현재 컨트랙트로 바뀌고 `msg.value`는 임의로 지정할 수 있습니다. 원래 발신자와 가치를 그대로 전달해야 할 때는 이 명령어가 감당하지 못하며, EIP-7이 발신자와 가치를 전파하는 방식으로 그 빈틈을 메웠습니다.

가치 의미의 혼란이 퇴장을 앞당겼습니다. 0이 아닌 `value`를 실은 CALLCODE는 현재 계정 잔액이 충분할 것을 요구하면서도 가치를 자기 주소로 보내고, 자식 호출은 바뀐 `msg.value`를 읽고, 장부상으로는 아무 이전도 일어나지 않습니다. 이런 동작은 직관으로 추론하기가 매우 어렵습니다. 실제 사용 상황에 대해서는 EIP-2488의 저자가 CALLCODE가 진짜로 쓰인 적이 한 번도 없다고 보았고, 프록시 패턴의 대규모 확산은 그보다 더 늦었습니다(DELEGATECALL은 Homestead에서 활성화되었고, EIP-7이 제시한 메인넷 활성화 블록은 1,150,000입니다). 이 두 가지는 공개된 타임라인과 저자 진술에 기반한 판단이며, 정량화할 수 있는 호출 통계가 부족합니다.

퇴장 과정은 두 단계입니다. Solidity는 0.5.0부터 `callcode`를 더 이상 허용하지 않아, 인라인 어셈블리만 여전히 `0xf2` 명령어를 낼 수 있습니다. 프로토콜 계층은 그 오프코드를 실제로 제거하지 않았고, EIP-2488은 CALLCODE가 특정 블록 높이 이후 항상 실패를 반환하게 하자고 제안합니다. 오프코드를 그냥 삭제하면 그것을 만난 컨트랙트가 비정상 중단되지만, 실패를 반환하면 컨트랙트가 인지하고 복구할 기회를 얻는다는 것이 이유입니다. 이 제안은 지금까지 정체(Stagnant) 상태에 머물러 있습니다. 결론은 이렇습니다. CALLCODE는 언어 수준에서는 이미 퇴장했고, 바이트코드 수준에서는 구현 측이 올바르게 처리해야 하는 유산 의미로 남아 있으며, 둘을 같은 것으로 뭉뚱그리면 안 됩니다.

## 호출 컨텍스트가 상태 쓰기가 어느 슬롯에 떨어지는지를 결정한다

네 명령어는 결국 같은 질문에 답합니다. 이 코드가 누구의 컨텍스트에서 실행되고 누구의 스토리지에 쓰이는가입니다. CALL과 STATICCALL은 상태를 호출 대상 컨트랙트에 쓰고, DELEGATECALL과 CALLCODE는 호출을 시작한 컨트랙트에 씁니다. 단일 스레드 실행에서는 이 차이가 정확성에만 영향을 주지만, 병렬 실행에서는 충돌 감지의 입력을 결정합니다. 스케줄러가 두 트랜잭션이 서로 간섭하는지 판단하려면 두 트랜잭션이 최종적으로 어떤 "주소와 스토리지 슬롯"의 조합에 쓰는지 알아야 하는데, 여기서 주소를 프록시로 잡을지 구현으로 잡을지를 결정하는 열쇠가 바로 호출 컨텍스트입니다. 프록시 패턴에서 한 번 전달할 때 쓰는 것은 프록시의 슬롯이지 구현 컨트랙트의 슬롯이 아닙니다. 충돌 감지가 같은 스토리지 슬롯을 둘러싸고 어떻게 전개되는지는 이후 storage layout과 읽기/쓰기 집합을 논할 때 다룹니다.

## 출처

- EIP-7 "DELEGATECALL", 오프코드 `0xf4`, 발신자와 가치 전파, Homestead 활성화 블록 1,150,000과 역사적 동기: https://eips.ethereum.org/EIPS/eip-7
- EIP-214 "New opcode STATICCALL", 정적 플래그, 금지 연산 목록, CALLCODE 예외: https://eips.ethereum.org/EIPS/eip-214
- EIP-609 "Hardfork Meta: Byzantium", EIP-211과 EIP-214를 비잔티움 포함 목록에 넣은 메타 제안: https://eips.ethereum.org/EIPS/eip-609
- EIP-211 "New opcodes: RETURNDATASIZE and RETURNDATACOPY", 동적 반환 데이터와 `BYZANTIUM_FORK_BLKNUM`: https://eips.ethereum.org/EIPS/eip-211
- EIP-150 "Gas cost changes for IO-heavy operations", 호출 기본 가격 700과 63/64 보존 규칙: https://eips.ethereum.org/EIPS/eip-150
- EIP-2929 "Gas cost increases for state access opcodes", 콜드 접근 2600, 웜 접근 100과 접근 집합: https://eips.ethereum.org/EIPS/eip-2929
- ERC-1967(원 EIP-1967) "Proxy Storage Slots", 구현 주소 슬롯 `keccak256("eip1967.proxy.implementation") - 1`: https://eips.ethereum.org/EIPS/eip-1967
- EIP-2488 "Deprecate the CALLCODE opcode", 항상 실패를 반환하게 하려는 동기, 상태는 정체(Stagnant)이며 미활성: https://eips.ethereum.org/EIPS/eip-2488
- Ethereum Yellow Paper, 부록 H의 CALL / CALLCODE / DELEGATECALL / STATICCALL 정의와 오프코드 표(CALLCODE의 `value ≤ 잔액` 전제 포함), 부록 G 수수료 표: https://ethereum.github.io/yellowpaper/paper.pdf
- Solidity 0.5.0 호환성 파괴 변경(`callcode` 더 이상 불가), 컨트랙트 문서에서 `view` / `pure`가 STATICCALL을 사용하고 라이브러리의 `view` 함수는 DELEGATECALL을 사용한다는 설명, 라이브러리 호출 보호와 `send()` / `transfer()` 권장 철회 설명: https://docs.soliditylang.org/en/v0.5.0/050-breaking-changes.html , https://docs.soliditylang.org/en/latest/contracts.html
- ethereum.org EVM 오프코드 참고와 wolflo/evm-opcodes 동적 gas 표, 가치 이전 9000, 새 계정 생성 25000, 2300 stipend: https://ethereum.org/en/developers/docs/evm/opcodes/ , https://github.com/wolflo/evm-opcodes/blob/main/gas.md

## 더 읽기

- ["EVM 호환성의 실제 의미: 바이트코드, 프리컴파일, JSON-RPC, 툴링"](/ko/blog/evm-compatibility-explained)
- ["충돌 핫스팟과 워크로드: 병렬 EVM이 실제로 도움이 될 때"](/ko/blog/parallel-evm-workload-hotspots)
- ["Bitroot 낙관적 병렬화: 감지, 재실행, 결정성"](/ko/blog/bitrootevm-)
