컨트랙트 업그레이드(Proxy) 시 발생할 수 있는 보안 사고 예방법

컨트랙트 업그레이드(Proxy) 시 발생할 수 있는 보안 사고 예방법 관련 이미지
안녕하세요! 10년 차 생활 블로거 김창수입니다. 오늘은 조금 전문적인 주제를 들고 왔는데요. 바로 블록체인 스마트 컨트랙트를 운영하면서 가장 가슴 철렁한 순간인 업그레이드 보안 사고에 대한 이야기예요. 제가 예전에 작은 프로젝트를 도와주다가 코드 한 줄 잘못 건드려서 식은땀을 흘렸던 기억이 나거든요.
보통 이더리움 같은 네트워크에서 컨트랙트는 한 번 배포하면 수정이 불가능하다고들 하잖아요? 하지만 프록시(Proxy) 패턴을 쓰면 로직을 바꿀 수 있게 되죠. 편리하긴 한데 이게 양날의 검이라서 자칫하면 수십억 원의 자산이 묶이거나 해킹당하는 사고가 빈번하게 발생하더라고요. 실제로 유명한 프로젝트들도 이 부분에서 실수를 많이 하곤 합니다.
제가 그동안 여러 문서를 뒤져보고 직접 겪으면서 배운 노하우를 바탕으로, 어떻게 하면 안전하게 컨트랙트를 업그레이드할 수 있는지 아주 상세하게 풀어보려고 해요. 개발자분들이나 프로젝트 운영자분들께 실질적인 도움이 되었으면 좋겠네요. 그럼 지금부터 하나하나 짚어보도록 할게요.
스토리지 충돌의 무서움과 예방법
컨트랙트 업그레이드에서 가장 흔하게 발생하는 사고가 바로 스토리지 충돌(Storage Collision)이에요. 프록시 구조에서는 데이터가 저장되는 공간과 실행되는 로직이 분리되어 있거든요. 새로운 로직 컨트랙트를 배포할 때 기존 변수 순서를 바꾸거나 중간에 끼워 넣으면 데이터가 엉망진창으로 꼬여버립니다.
예를 들어, 첫 번째 변수가 주소(address)였는데 업그레이드하면서 그 자리에 숫자(uint256)를 넣어버리면, 기존에 저장된 주소 데이터가 숫자로 해석되는 대참사가 벌어져요. 이런 실수를 막으려면 변수 선언 순서를 절대 바꾸지 말아야 합니다. 기존 변수들은 그대로 두고 아래쪽에만 새로운 변수를 추가하는 게 국룰이더라고요.
또 하나 중요한 건 Storage Gap을 활용하는 거예요. 미리 빈 공간을 예약해두는 방식인데, uint256[50] __gap; 같은 형태로 선언해두면 나중에 변수를 추가할 때 이 공간을 깎아서 쓸 수 있어 안전하답니다. 이걸 안 해두면 나중에 부모 컨트랙트가 바뀌었을 때 자식 컨트랙트의 데이터 위치가 밀려버리는 사고가 나기 딱 좋거든요.
프록시 패턴별 장단점 비교
업그레이드 방식을 정할 때 보통 세 가지 패턴을 두고 고민하게 됩니다. 제가 직접 써보니 각각 장단점이 뚜렷하더라고요. 가장 대중적인 건 투명 프록시(Transparent Proxy)지만, 최근에는 가스비를 아끼려고 UUPS 방식을 더 선호하는 추세인 것 같아요.
다이아몬드 패턴 같은 경우는 대규모 프로젝트에서 기능을 쪼개 관리할 때 유용하긴 한데, 구현 난이도가 너무 높아서 초보자가 쓰기엔 좀 벅찬 감이 있습니다. 아래 표를 보면서 우리 프로젝트에 맞는 게 뭔지 한번 생각해보시면 좋을 것 같아요.
| 구분 | Transparent (투명) | UUPS | Diamond (다이아몬드) |
|---|---|---|---|
| 업그레이드 로직 위치 | Proxy 컨트랙트 | Implementation 컨트랙트 | Facet 컨트랙트별 분산 |
| 가스 효율성 | 보통 (매 호출시 체크) | 높음 (체크 비용 적음) | 매우 높음 (필요 기능만 호출) |
| 복잡도 | 낮음 (표준적) | 중간 (주의 필요) | 매우 높음 (관리 어려움) |
| 사고 위험성 | 상대적으로 낮음 | 높음 (로직 누락 시 업글 불가) | 중간 (함수 충돌 가능성) |
투명 프록시는 관리자 권한 충돌 문제를 자동으로 해결해주니까 초보 개발자에게 참 친절한 방식이에요. 반면 UUPS는 로직 컨트랙트 안에 업그레이드 함수를 깜빡하고 안 넣으면 다시는 업데이트를 못 하는 영구 동결 상태가 될 수 있어서 정말 조심해야 하더라고요.
나의 뼈아픈 초기화 실패담
여기서 제 부끄러운 과거 이야기를 하나 해드릴게요. 프록시 컨트랙트는 일반적인 constructor를 사용할 수 없거든요. 대신 initialize라는 별도의 함수를 만들어서 초기 설정을 해줘야 합니다. 그런데 제가 예전에 한 번은 이 함수에 initializer 제어자를 붙이는 걸 깜빡한 적이 있어요.
결과가 어땠을까요? 누구나 그 함수를 호출해서 컨트랙트의 관리자 권한을 뺏어갈 수 있는 상태가 되어버렸죠. 다행히 메인넷 배포 전 테스트넷 단계에서 팀원이 발견해서 망정이지, 그대로 배포했다면 제 블로거 인생도 끝날 뻔했답니다. 이 사건 이후로 저는 초기화 함수만큼은 눈이 빠지도록 검토하는 습관이 생겼어요.
특히 업그레이드를 여러 번 진행할 때는 reinitializer를 써서 버전에 맞게 초기화가 되었는지 확인하는 과정이 필수적입니다. 이전 버전의 초기화 로직이 다시 실행되지 않도록 막는 것도 보안의 핵심이더라고요. 여러분은 저 같은 실수 절대 하지 마시고, 꼭 라이브러리에서 제공하는 표준 제어자를 사용하시길 바랄게요.
실무에서 바로 쓰는 보안 체크리스트
사고를 막기 위해 제가 항상 지키는 철칙들이 있습니다. 가장 먼저, 멀티시그(Multi-sig) 지갑 사용을 권장해요. 혼자서 업그레이드 권한을 다 가지고 있으면 개인 키 하나만 털려도 프로젝트 전체가 무너지거든요. 팀원 여럿의 승인이 있어야 업그레이드가 가능하도록 구조를 짜는 게 훨씬 안전합니다.
두 번째는 타임락(Time-lock) 설정이에요. 업그레이드 예고를 하고 실제 적용까지 48시간 정도 지연 시간을 두는 거죠. 이렇게 하면 혹시 모를 악의적인 공격이 감지되었을 때 사용자들이 자산을 대피시킬 시간을 벌 수 있습니다. 프로젝트의 신뢰도를 높이는 데에도 큰 도움이 되더라고요.
마지막으로 자동화된 검증 도구를 적극 활용하세요. OpenZeppelin에서 제공하는 업그레이드 플러그인을 쓰면 스토리지 레이아웃이 깨졌는지 미리 확인해줍니다. 사람이 눈으로 확인하는 건 한계가 있으니까, 이런 도구들의 도움을 받는 게 정신 건강에도 좋고 사고 예방에도 탁월합니다.
자주 묻는 질문
Q. 컨트랙트 업그레이드 권한은 누가 가지는 게 가장 좋나요?
A. 개인 지갑보다는 Gnosis Safe 같은 멀티시그 지갑을 관리자로 지정하는 것이 보안상 가장 강력합니다. 최소 3명 이상의 승인을 받도록 설정하는 것을 추천드려요.
Q. 왜 컨트랙트에서 constructor를 쓰면 안 되나요?
A. 프록시 패턴에서는 데이터가 프록시 컨트랙트에 저장되는데, 로직 컨트랙트의 constructor는 배포 시점에만 실행되어 프록시의 스토리지에 영향을 주지 못하기 때문입니다.
Q. 스토리지 갭(Storage Gap)의 크기는 어느 정도가 적당한가요?
A. 보통 OpenZeppelin 표준에 따라 50개의 uint256 슬롯을 비워두는 것이 일반적입니다. 향후 추가될 변수가 많을 것 같다면 더 넉넉하게 잡아도 괜찮아요.
Q. 업그레이드 후에 이전 로직 컨트랙트는 어떻게 되나요?
A. 블록체인 상에 기록으로 남아는 있지만, 프록시가 가리키는 주소가 바뀌었으므로 더 이상 사용되지 않는 상태가 됩니다.
Q. 셀프디스트럭트(selfdestruct) 함수를 써도 되나요?
A. 프록시 구조에서 로직 컨트랙트에 selfdestruct가 있으면 큰일 납니다. 누군가 이를 실행하면 로직이 사라져 프록시가 먹통이 될 수 있거든요. 절대 피해야 할 패턴입니다.
Q. UUPS 패턴에서 업그레이드 함수를 빼먹으면 어떻게 하나요?
A. 안타깝게도 해당 컨트랙트는 영원히 업그레이드가 불가능해집니다. 배포 전에 반드시 업그레이드 관련 인터페이스가 포함되었는지 자동화 테스트로 검증해야 합니다.
Q. 프록시를 쓰면 가스비가 얼마나 더 나오나요?
A. delegatecall을 한 번 더 거치기 때문에 약 1,000 ~ 2,000 가스 정도가 추가로 소모됩니다. 사용자 입장에서 크게 체감될 정도는 아니지만 빈번한 호출이 있다면 고려 대상이 될 수 있어요.
Q. 업그레이드 가능한 컨트랙트도 오딧(Audit)을 받아야 하나요?
A. 네, 필수입니다. 오히려 수정이 가능하기 때문에 보안 취약점이 생길 가능성이 더 높거든요. 특히 스토리지 레이아웃 검증을 포함한 전문 오딧을 권장합니다.
컨트랙트 업그레이드는 분명 매력적인 도구지만, 그만큼 책임이 따르는 작업이라는 걸 다시 한번 느끼네요. 제가 공유해드린 내용들이 여러분의 소중한 프로젝트를 지키는 데 조금이나마 보탬이 되었으면 좋겠습니다. 기술적인 부분도 중요하지만 결국 꼼꼼한 확인과 절차를 지키는 마음가짐이 제일 큰 보안책이더라고요.
오늘 글이 유익하셨다면 주변 동료분들께도 공유 부탁드려요. 더 궁금한 점이 생기면 언제든지 댓글 남겨주시고요. 저는 다음에 더 알차고 재미있는 생활 밀착형 IT 정보로 돌아오겠습니다. 긴 글 읽어주셔서 정말 감사해요!
작성자: 생활 블로거 김창수
블록체인과 일상의 경계를 허무는 10년 차 에디터입니다. 복잡한 기술을 쉽게 풀어서 설명하는 것을 좋아합니다.
본 포스팅은 정보 제공만을 목적으로 하며, 특정 기술의 도입이나 투자를 권유하지 않습니다. 컨트랙트 배포 및 운영에 따른 모든 책임은 사용자 본인에게 있음을 알려드립니다.
댓글
댓글 쓰기