Delegatecall 프록시는 구현 코드가 프록시 storage를 읽고 쓰므로 새 구현의 변수 layout이 이전과 byte 단위로 호환돼야 합니다. 기존 변수 앞에 새 변수를 넣거나 순서를 바꾸고, type·struct member·상속 순서를 변경하면 같은 slot과 offset을 다른 의미로 읽어 잔액·owner·mapping base가 손상될 수 있습니다. 안전한 기본은 기존 선언을 그대로 두고 끝에 새 변수를 추가하며 compiler의 storageLayout JSON에서 label뿐 아니라 slot, offset, type encoding을 이전 build와 비교하는 것입니다.
Delegatecall은 구현의 layout으로 프록시 값을 읽습니다
Proxy slot 0에 owner, slot 1에 balances mapping base가 있다고 하겠습니다. 새 implementation이 slot 0에 paused를 추가하면 기존 owner address의 하위 byte를 bool로 읽고 새 owner는 다음 slot을 읽습니다. 함수 selector와 ABI가 같아도 authorization과 자산 장부가 즉시 달라집니다.
Solidity는 value types를 32바이트 slot 안 낮은 order부터 pack합니다. 작은 type 하나를 중간에 넣으면 뒤 변수의 offset이 이동할 수 있습니다. Struct와 array는 새 slot에서 시작하고 mapping·dynamic array는 선언 slot을 base로 Keccak 위치를 계산하므로 base slot 변화가 모든 원소 주소를 바꿉니다.
업그레이드 호환성은 변수 이름이 남았는지가 아니라 기존 모든 byte를 새 코드가 같은 의미로 읽는지의 문제입니다.
JOBCOIN 해설

그림은 이 주제의 공통 개념을 단순화한 설명입니다. 아래 항목에서 이 글의 구체적인 조건과 예외를 함께 읽어보세요.
- Delegatecall은 구현의 layout으로 프록시 값을 읽습니다
Proxy slot 0에 owner, slot 1에 balances mapping base가 있다고 하겠습니다.
- 변경 유형별 위험을 구분합니다
끝에 새 uint256을 추가하는 변경은 보통 기존 slot을 유지하지만 storage gap 사용, inheritance와 custom layout이 있으면 실제 compiler output을 확인해야 합니다.
- Compiler JSON을 byte 위치 기준으로 비교합니다
Solidity standard JSON의 storage layout output은 각 항목의 label, slot, offset과 type id를 제공하고 types에는 encoding과 numberOfBytes, struct members가 있습니다.
AI로 제작한 개념도 · 실제 가격·거래 내역·통계가 아닙니다.
변경 유형별 위험을 구분합니다
끝에 새 uint256을 추가하는 변경은 보통 기존 slot을 유지하지만 storage gap 사용, inheritance와 custom layout이 있으면 실제 compiler output을 확인해야 합니다. 기존 uint128을 uint256으로 넓히면 같은 slot의 이웃 값을 덮을 수 있고 enum·struct 정의 변경도 type encoding을 바꿉니다.
Base contract의 변수 추가나 상속 순서 변경은 C3 linearization에 따른 전체 배치를 움직일 수 있습니다. Constant와 immutable은 일반 storage layout에 같은 방식으로 들어가지 않지만 새 구현 bytecode의 값과 초기화 의미를 별도로 검토합니다.
| 변경 | 기존 slot 영향 | 판정 |
|---|---|---|
| 끝에 변수 추가 | 대개 유지 | layout diff 필요 |
| 중간 삽입·재정렬 | 뒤 slot·offset 이동 | 금지 |
| 기존 type 변경 | 폭·encoding 변경 | 금지 |
| struct member 변경 | 내부 offset 이동 | 기존 struct 위험 |
| base contract 변경 | 상속 전체 배치 변화 | 전체 diff |
Compiler JSON을 byte 위치 기준으로 비교합니다
Solidity standard JSON의 storage layout output은 각 항목의 label, slot, offset과 type id를 제공하고 types에는 encoding과 numberOfBytes, struct members가 있습니다. 숫자처럼 보이는 slot도 큰 값이 가능하므로 문자열을 임의 JavaScript Number로 바꾸지 않습니다.
비교기는 변수명이 바뀐 경우도 같은 slot의 의미 변경으로 경고하고 mapping key·value, array base와 struct member를 재귀 비교해야 합니다. 이 JSON 형식 자체는 experimental 표시가 있으므로 compiler version을 고정하고 tool parser 회귀 테스트를 유지합니다.
- 이전·새 compiler와 optimizer 설정을 고정합니다.
- Storage layout JSON을 artifact로 저장합니다.
- Slot·offset·encoding·byte width를 재귀 비교합니다.
- Proxy 실제 state를 fork해 upgrade transaction을 실행합니다.
- 핵심 잔액·owner·allowance invariant를 전후 검산합니다.
Gap과 namespace도 자동 안전장치는 아닙니다
상속 가능한 upgradeable base는 예약 배열 gap을 두고 이후 변수를 그 공간에 배치할 수 있습니다. 새 변수 크기만큼 gap을 정확히 줄여 전체 후속 slot을 유지해야 하며 gap을 없애거나 잘못 계산하면 충돌합니다. Tool이 gap 규칙을 인식하는지 확인합니다.
Namespaced storage는 고유 namespace에서 계산한 base slot 아래 struct를 두어 모듈 충돌을 줄일 수 있습니다. 그러나 namespace 재사용, struct member 재정렬과 잘못된 assembly slot은 여전히 상태를 깨뜨립니다. Pattern 이름보다 계산된 slot과 layout diff가 증거입니다.
Rollback보다 사전 simulation이 중요합니다
잘못된 implementation이 한번 delegatecall되어 storage를 덮으면 이전 implementation으로 되돌려도 원래 bytes가 자동 복구되지 않습니다. Upgrade 권한과 timelock은 악성 변경 기회를 줄이지만 layout 실수를 치료하지 않습니다.
Mainnet state fork에서 upgrade 전후 getter만 비교하지 말고 raw storage와 protocol invariant, 과거 user positions를 표본 검사합니다. Upgrade call과 initializer가 한번에 실행되는 경우 중간 상태·권한과 reentrancy도 포함합니다.
자주 묻는 질문
변수 이름만 바꾸면 layout이 깨지나요?
Slot·offset·type이 같으면 bytes 배치는 유지될 수 있지만 tool·ABI와 운영 의미가 바뀌므로 명시적 rename 검토가 필요합니다.
새 변수는 항상 맨 끝에 넣으면 안전한가요?
상속·gap·namespace와 compiler 설정에 따라 달라 실제 layout diff가 필요합니다.
Upgrade를 되돌리면 손상된 상태도 돌아오나요?
아닙니다. 이미 잘못 쓴 storage는 별도 복구 transaction 없이는 원복되지 않습니다.
더 깊이 읽기
본문에서 다룬 개념과 확인 절차를 다음 글에서 이어서 살펴보세요.



