교육 목적으로만 제공되며 투자 조언이 아닙니다. 투자로 손실이 발생할 수 있습니다.
직접 답변
프록시 스토리지 충돌은 구현 코드가 기존 상태를 만든 레이아웃과 다른 의미로 프록시의 스토리지 슬롯을 읽거나 쓸 때 발생합니다. 업그레이드로 인해 잔액이 주소로 해석되거나, 소유자가 지워지거나, 매핑의 기준 슬롯 또는 업그레이드 제어 데이터가 손상될 수 있습니다.
delegatecall을 사용하면 구현 바이트코드는 프록시의 컨텍스트에서 실행됩니다. 스토리지, 잔액, address(this)는 프록시에 속합니다. 변수 이름은 온체인에 저장되지 않으므로 EVM은 새 코드가 계산한 슬롯과 바이트 오프셋만 따릅니다.
따라서 업그레이드는 단순히 컴파일에 성공하거나 같은 함수를 노출하는 데 그치지 않고 배포된 레이아웃을 보존해야 합니다. 승인하기 전에 컴파일러가 생성한 레이아웃을 실제 배포된 정확한 버전과 비교해야 합니다.
작동 원리
Solidity는 일반적으로 상속 관계를 C3 선형화한 뒤 선언 순서에 따라 슬롯 0부터 상태 변수를 배치합니다. 32바이트보다 작은 값은 한 슬롯을 공유할 수 있습니다. 구조체와 배열에는 추가 배치 규칙이 있으며, 매핑과 동적 배열은 기준 슬롯에서 데이터 위치를 계산합니다. 기준 슬롯이 이동하면 파생 데이터의 위치도 바뀝니다.
감사해야 할 충돌 경계는 다음 네 종류입니다.
- 프록시와 구현: 구현 주소나 관리자 같은 프록시 고유 필드는 애플리케이션 상태가 사용하는 슬롯을 차지하면 안 됩니다. ERC-1967은 구현, 비콘, 관리자 데이터에 대해 일반적인 컴파일러 할당을 피하는 표준 슬롯을 정의합니다.
- 이전 구현과 새 구현: 기존 애플리케이션 변수는 호환되는 슬롯, 오프셋, 타입을 유지해야 합니다. 끝에 변수를 추가하는 것은 안전할 수 있지만, 삽입, 순서 변경, 삭제 또는 타입 변경은 기존 워드를 다른 의미로 해석하게 만듭니다.
- 상속: 기본 컨트랙트에 상태를 추가하거나 상속 순서를 바꾸면 파생 컨트랙트 소스가 그대로여도 그 스토리지가 이동할 수 있습니다.
- 예약 또는 네임스페이스 스토리지: 올바르게 사용하는 스토리지 갭은 기본 컨트랙트용 공간을 예약하고 ERC-7201 방식의 네임스페이스는 레이아웃을 분리할 수 있습니다. 어느 방식도 기존 레이아웃 내부의 임의 변경을 허용하지 않습니다.
예시
버전 1의 레이아웃이 다음과 같다고 가정합니다.
uint256 totalAssets; // slot 0
address owner; // slot 1버전 2가 잘못해서 맨 앞에 변수를 삽입합니다.
bool paused; // slot 0, offset 0
uint256 totalAssets; // slot 1
address owner; // slot 2업그레이드 후 paused는 이전 totalAssets의 하위 바이트를 읽고, 새 totalAssets는 이전 owner 워드를 정수로 읽으며, owner는 슬롯 2에 이미 있던 값, 흔히 영을 읽습니다. 원래 워드는 스토리지에 남아 있지만 새 코드가 다른 의미를 부여합니다. 트랜잭션은 성공하면서도 잘못 해석된 상태로 권한 검사나 회계 로직을 수행할 수 있습니다.
위험과 업그레이드 점검
- 두 구현의 스토리지 레이아웃 출력을 생성하고 실제 배포된 참조 컨트랙트를 기준으로 슬롯, 오프셋, 타입, 상속 정보를 비교합니다.
- 기존 선형 레이아웃에서는 새 변수를 끝에만 추가합니다. 호환성을 입증하지 못했다면 기존 필드의 순서나 타입을 바꾸거나, 삭제 후 재사용하거나, 기본 컨트랙트를 수정하지 마십시오.
- 스토리지 갭을 사용할 때는 실제 사용한 예약 슬롯 수만큼 정확히 줄입니다. 네임스페이스 스토리지에서는 식별자를 고유하게 유지하고 기존 각 네임스페이스 내부의 변경을 검증합니다.
- ERC-1967을 완전한 보호로 보지 마십시오. 프록시 메타데이터를 컴파일러가 할당한 애플리케이션 슬롯과 분리할 뿐, 두 구현 레이아웃을 호환되게 만들지는 않습니다.
- 포크 또는 상태 스냅샷에서 업그레이드와 재초기화 함수를 테스트합니다. 실행 전후의 소유자, 역할, 잔액, 허용량, 매핑 항목, 일시정지 상태, 구현 슬롯, 롤백 또는 긴급 제어를 확인합니다.
업그레이드가 이미 충돌을 일으킨 것으로 의심되면 거버넌스가 허용하는 범위에서 추가 업그레이드와 상태 변경 호출을 중단하십시오. 업그레이드 전 블록 번호와 구현 바이트코드를 보존하고, 영향을 받은 슬롯의 원시 스토리지를 비교하며, 자격을 갖춘 컨트랙트 엔지니어가 마이그레이션을 설계하고 독립 검토를 받게 해야 합니다. 검증된 스토리지 맵 없이 업그레이드를 반복하면 복구 가능한 상태를 더 많이 파괴할 수 있습니다.
흔한 오해
- “변수 이름이 같으니 레이아웃은 안전하다.” 이름은 스토리지 위치를 정하지 않습니다. 타입, 순서, 패킹, 상속, 네임스페이스 규칙이 결정합니다.
- “변수를 삭제하면 슬롯을 재사용할 수 있다.” 프록시 스토리지는 계속 남습니다. 엄격히 검토한 마이그레이션으로 이전 값을 지우거나 변환하지 않는다면 재사용은 이전 워드에 새 의미를 부여할 뿐입니다.
- “테스트 트랜잭션 하나가 성공하면 업그레이드는 호환된다.” 테스트는 일부 슬롯만 건드릴 수 있습니다. 레이아웃 검증과 상태 차이 테스트는 권한 필드, 패킹된 값, 매핑, 배열, 상속 스토리지를 포괄해야 합니다.
관련 주제
출처
- 스마트 컨트랙트 소개 - Solidity Documentation (접속일: 2026-08-21)
- 스토리지와 임시 스토리지의 상태 변수 레이아웃 - Solidity Documentation (접속일: 2026-08-21)
- ERC-1967: 프록시 스토리지 슬롯 - Ethereum Improvement Proposals (접속일: 2026-08-21)
- 업그레이드 가능한 컨트랙트 작성 - OpenZeppelin Documentation (접속일: 2026-08-21)