아무도 부르지 않은 함수, fallback이 응답하는 이유 morgan021 2025. 10. 29.
> _
스마트 컨트랙트는 코드로 이루어진 견고한 성채다. 이 성에는 transfer, approve, balanceOf처럼 명확한 이름표가 붙은 여러 개의 정문이 존재한다. 사용자와 다른 계약들은 이 정문들을 통해 예측 가능하고 투명한 방식으로 자신의 용무를 처리한다. 모든 것이 명확한 규칙 아래 움직이는 이 디지털 요새에서, 대부분의 상호작용은 이 이름표 붙은 함수들을 통해 이루어진다.
하지만 이 성에는 정문으로 들어오지 못하는, 혹은 정문이 아예 존재하지 않는 방문객들을 처리하는 단 하나의 비밀스러운 통로가 있다. 이름 없는 호출, 길 잃은 데이터, 혹은 그저 성의 금고에 돈(이더)을 넣으려는 순수한 방문객들. 이 모든 예측 불가능한 요청을 묵묵히 받아내는 존재, 그것이 바로 fallback 함수다.
이름 그대로, 이것은 '최후의 보루'다. 계약에 정의된 그 어떤 함수 시그니처와도 일치하지 않는 모든 호출이 이곳으로 밀려온다. fallback은 단순한 예외 처리기나 오류 페이지가 아니다. 이것은 계약의 유연성과 확장성을 책임지는 핵심 메커니즘이자, 동시에 잘못 다루면 성 전체를 무너뜨릴 수 있는 가장 위험한 관문이다. 이 만능 문지기가 정확히 언제, 그리고 왜 호출되는지 이해하는 것은 이더리움 생태계의 작동 원리를 꿰뚫는 첫걸음이다.
구시대의 만능 열쇠: 이더를 받던 유일한 통로
솔리디티 0.6 버전 이전, fallback 함수의 역할은 지금보다 훨씬 더 광범위하고 중요했다. 그 시절 이 함수는 계약이 순수한 이더(ETH)를 받을 수 있는 거의 유일한 방법이었다.
누군가가 컨트랙트 주소로 아무런 데이터 없이 오직 이더만 전송한다고 상상해 보자. 이 행위는 어떤 특정한 함수를 호출하는 것이 아니다. 그저 '돈을 보내는' 행위다. 이때 계약이 이 돈을 받아서 자신의 잔고에 추가하려면, 이 상황을 처리할 함수가 필요했다. 그 역할을 fallback 함수가 담당했다.
당시의 fallback 함수는 두 가지 주요 임무를 동시에 수행했다. 첫째, 데이터 없이 이더만 전송될 때(순수 이더 전송) 이를 수신했다. 둘째, 계약에 존재하지 않는 함수가 호출될 때(데이터 포함) 그 요청을 받았다. 이 두 가지 이질적인 시나리오가 하나의 함수로 귀결되다 보니, 개발자는 fallback 내부에서 msg.data.length == 0과 같은 조건문을 사용해 지금이 순수 이더 전송인지, 아니면 잘못된 함수 호출인지 구분해야만 했다. 이는 코드를 혼란스럽게 만들고 잠재적인 버그의 원인이 되었다.
역할의 분담: receive 함수의 등장
이러한 혼란을 해결하기 위해 솔리디티 0.6 버전에서 중대한 변화가 일어났다. fallback 함수의 과중한 업무를 덜어줄 전문가, receive 함수가 도입된 것이다.
receive 함수는 이름 그대로 오직 하나의 목적만을 위해 존재한다. 바로 아무런 데이터(calldata) 없이 순수한 이더(ETH)만 전송받는 것. 이 함수는 receive() external payable { ... }이라는 명확한 시그니처를 가지며, 다른 어떤 인자도 받지 않는다.
이 전문가의 등장으로 계약의 입구는 훨씬 더 체계적으로 정리되었다.
- 시나리오 1 (순수 이더 전송): 사용자가 data 없이 이더만 보낸다.
- 만약 계약에 receive 함수가 존재한다면, receive 함수가 이 호출을 받는다.
- 만약 receive 함수가 없고 fallback 함수가 payable로 존재한다면, 예전처럼 fallback 함수가 이 호출을 받는다.
- 만약 receive와 payable fallback 둘 다 없다면, 트랜잭션은 실패(revert)한다.
- 시나리오 2 (데이터 포함 호출): 사용자가 data를 포함하여 계약을 호출한다.
- 이 data가 계약 내의 어떤 함수(예: transfer)와 일치하면, 해당 함수가 실행된다.
- 만약 일치하는 함수가 아무것도 없다면, 이때 비로소 fallback 함수가 호출된다.
현대 솔리디티에서 fallback의 주된 임무는 더 이상 단순 이더 수신이 아니다. receive라는 전문가에게 그 일을 맡기고, fallback은 일치하는 함수가 없는 모든 데이터 기반 호출을 처리하는 본연의 '최후의 보루' 역할에 집중하게 되었다.
존재하지 않는 호출: 프록시의 심장이 되다
fallback이 일치하지 않는 모든 데이터 호출을 처리한다는 것은, 이 기능이 업그레이드 가능한 '프록시(Proxy) 컨트랙트'의 핵심 엔진으로 작동할 수 있음을 의미한다.
프록시 컨트랙트는 일종의 '대문'이다. 사용자는 항상 이 '대문' 주소와 상호작용한다. 실제 토큰 로직(예: transfer, approve)은 '구현 계약'이라는 별도의 '집'에 존재한다. 이 '대문' 계약 자체에는 transfer나 approve 같은 함수가 정의되어 있지 않다.
그렇다면 사용자가 '대문' 주소에 대고 transfer를 호출하면 무슨 일이 벌어질까?
- 사용자가 transfer(to, amount) 호출을 '대문'(프록시)에 보낸다.
- '대문' 계약은 자신의 함수 목록을 확인한다. transfer라는 함수는 존재하지 않는다.
- 일치하는 함수가 없으므로, 이 호출은 fallback 함수로 향한다.
- 프록시의 fallback 함수는 이 호출을 거부하는 대신, delegatecall이라는 특수한 명령을 통해 이 요청(transfer 호출 데이터)을 '집'(구현 계약)으로 그대로 전달한다.
- '집' 계약의 transfer 로직이 '대문'의 저장소(장부) 위에서 실행된다.
이 구조에서 fallback은 더 이상 예외 상황을 처리하는 함수가 아니다. 프록시 계약의 모든 핵심 기능을 처리하는 메인 엔진이자 유일한 관문이 된다.
payable: 돈을 받기 위한 허가증
fallback과 receive 모두에게 공통적으로 적용되는 중요한 규칙이 있다. 만약 이 함수들이 이더를 받아야 한다면, 반드시 payable 키워드로 선언되어야 한다는 것이다.
payable은 일종의 '입금 허가증'이다. 이 키워드가 없다면, 계약은 msg.value(전송된 이더)가 0보다 클 경우 즉시 트랜잭션을 거부한다. 개발자가 payable을 명시하는 행위는 "나는 이 함수가 이더를 받을 것임을 인지하고 있으며, 그로 인한 모든 보안적 영향을 감수하겠다"고 선언하는 것과 같다.
만약 receive 함수가 payable이 아니라면, 그 계약은 순수 이더 전송을 받을 수 없다. 만약 fallback 함수가 payable이 아니고 receive도 없다면, 그 계약 역시 이더를 받을 수 없다. 프록시 컨트랙트의 fallback이 payable이라면, 사용자는 transfer 같은 함수 호출과 동시에 이더를 전송하는(예: WETH를 래핑하는) 동작도 수행할 수 있다.
문지기의 위험성: 가스와 재진입
이처럼 강력한 fallback은 두 가지 치명적인 위험을 안고 있다. 바로 가스(Gas) 문제와 재진입(Re-entrancy) 공격이다.
과거에는 .transfer()나 .send() 함수를 사용해 계약에 이더를 보냈다. 이 함수들은 보안상의 이유로 단 2300 가스만 전달했다. 이는 이더를 받는 fallback 함수가 복잡한 로직(예: 스토리지 쓰기)을 수행하기에 턱없이 부족한 양이었다. 만약 fallback이 2300 가스보다 많은 일을 하도록 설계되었다면, 이더 전송은 실패했다.
이 문제를 해결하기 위해 현대에는 recipient.call{value: amount}("") 방식이 표준이 되었다. 이 방식은 현재 사용 가능한 모든 가스를 fallback(혹은 receive) 함수로 전달한다. 이는 fallback이 복잡한 로직을 수행할 수 있게 해주었지만, 동시에 '재진입 공격'이라는 최악의 문을 열어젖혔다.
만약 fallback 함수가 자신을 호출한 계약의 또 다른 함수를 '다시 호출(re-enter)'할 수 있다면, 공격자는 자신의 잔액이 차감되기도 전에 반복적으로 출금 함수를 호출하여 계약의 모든 자금을 탈취할 수 있다.
단순한 '기타'가 아닌 핵심 관문
fallback 함수는 솔리디티의 '기타' 항목이 아니다. 그것은 계약의 유연성, 확장성, 그리고 위험성을 동시에 상징하는 핵심적인 메커니즘이다.
단순 이더 수신이라는 과거의 역할에서 벗어나, 현대의 fallback은 특히 프록시 패턴에서 계약의 모든 상호작용을 처리하는 심장부 역할을 수행한다. fallback이 없이는 업그레이드 가능한 컨트랙트라는 개념 자체가 성립하기 어렵다.
그러나 이 만능 문지기는 맹목적이다. 그는 자신에게 전달된 모든 것을 받아들이고 처리하려 한다. 이 문을 어떻게 설계하고 통제하는지, payable을 언제 허용할지, 그리고 재진입이라는 유령으로부터 이 문을 어떻게 방어할지에 따라 이 견고한 성은 영원한 요새가 될 수도, 혹은 한순간에 무너지는 모래성이 될 수도 있다.
3줄 요약
[1] fallback 함수는 계약에 일치하는 함수가 없을 때 모든 호출을 받는 최후의 보루다.
[2] 솔리디티 0.6 이후 receive가 순수 이더 전송을, fallback이 데이터가 포함된 호출을 주로 처리하도록 역할이 분담되었다.
[3] fallback은 프록시 패턴의 핵심 엔진으로 작동하며, delegatecall을 통해 모든 요청을 구현 계약으로 위임하는 관문 역할을 한다.
'MACHINE: EXPLOIT' 카테고리의 다른 글
| 인터넷이 정보를 해방시켰다면, IBC는 자산을 해방시킨다 (0) | 2025.11.05 |
|---|---|
| EVM은 왜 미래를 예측할 수 없는가 (0) | 2025.11.04 |
| Code is Law의 배신, 그리고 프록시라는 구원 (0) | 2025.10.31 |
| 가장 비싼 땅, 이더리움에서 28바이트를 아끼는 기술 (0) | 2025.10.31 |
| 우아한 포장지의 비밀, transfer는 왜 _transfer를 부를까? (0) | 2025.10.30 |
| EVM의 DELEGATECALL은 누구의 시점으로 세상을 보는가 (0) | 2025.10.28 |
| 탈중앙화라는 완벽한 환상과 그것을 깨뜨리는 관리자 (0) | 2025.10.26 |
| 당신의 코드는 당신의 것인가? 바이브 코딩이 초래하는 결말 (0) | 2025.10.25 |
| 당신의 AI는 당신을 속이고 있다: 간신배를 박살내는 3가지 심문 기술 (0) | 2025.10.25 |
| 대 AI 시대, 우리는 무엇으로 존재하는가? (0) | 2025.10.25 |