
리스크에서 보호까지: TON 스마트 계약의 보안 취약점과 최적화 제안
글: Beosin
블록체인 기술이 빠르게 발전하는 오늘날, TON(The Open Network)은 효율적이고 유연한 블록체인 플랫폼로서 점점 더 많은 개발자의 주목을 받고 있습니다. TON의 독특한 아키텍처와 특성은 탈중앙화 애플리케이션 개발에 강력한 도구와 풍부한 가능성을 제공합니다.
그러나 기능과 복잡성이 증가함에 따라 스마트 계약의 보안성 또한 점점 더 중요해지고 있습니다. TON에서 스마트 계약을 위한 프로그래밍 언어인 FunC는 그 유연성과 효율성으로 유명하지만, 동시에 잠재적인 위험과 도전 과제도 수반합니다. 안전하고 신뢰할 수 있는 스마트 계약을 작성하기 위해서는 개발자가 FunC 언어의 특성과 존재 가능한 리스크를 깊이 이해해야 합니다.
본 문서에서는 TON 블록체인상의 스마트 계약 관련 특징들과, TON 상에서 자주 간과되기 쉬운 스마트 계약의 취약점을 자세히 분석합니다.

TON 비동기 특성 및 계정 메커니즘 분석
스마트 계약의 비동기 호출
네트워크 샤딩 및 비동기 통신
TON 블록체인은 설계상 세 가지 체인으로 나뉩니다: 메인체인(Masterchain), 워킹체인(Workingchains), 샤드체인(Shardchains).
메인체인은 전체 네트워크의 핵심으로, 전망의 메타데이터와 컨센서스 메커니즘을 저장합니다. 모든 워킹체인과 샤드체인의 상태를 기록하며, 전망의 일관성과 보안성을 보장합니다. 워킹체인은 최대 2^32개까지 존재할 수 있는 독립된 블록체인으로, 특정 유형의 트랜잭션과 스마트 계약을 처리합니다. 각 워킹체인은 서로 다른 애플리케이션 요구에 맞추어 자체 규칙과 특성을 가질 수 있습니다. 샤드체인은 워킹체인의 서브체인으로, 워킹체인의 부하를 추가로 분할하여 처리 능력과 확장성을 높입니다. 각 워킹체인은 최대 2^60개의 샤드체인으로 분할되며, 각각은 일부 트랜잭션을 독립적으로 처리함으로써 고효율의 병렬 처리를 실현합니다.
이론적으로 각 계정은 하나의 샤드체인을 독점할 수 있으며, 각 계정은 자신의 COIN/TOKEN 잔액을 독립적으로 관리하고, 계정 간 거래는 완전히 병렬로 수행될 수 있습니다. 계정 간에는 비동기 메시지를 통해 정보가 전달되며, 샤드체인 간 메시지 전달 경로는 log_16(N) - 1이며, 여기서 N은 샤드체인의 수입니다.

이미지 출처: https://frontierlabzh.medium.com/ton-web3%EC%84%B8%EA%B3%84%EC%9D%98%20weixin-e1d3ae3b3574
TON에서 스마트 계약은 메시지를 보내고 받는 방식으로 상호작용합니다. 이러한 메시지는 내부 메시지(일반적으로 스마트 계약 간 상호작용 시 전송되는 메시지) 또는 외부 메시지(외부 소스로부터 전송된 메시지)일 수 있습니다. 메시지 전달 과정은 대상 계약의 즉각적인 응답을 기다릴 필요가 없으며, 송신 측은 나머지 로직 코드를 계속 실행할 수 있습니다. 이와 같은 비동기 메시지 전달 메커니즘은 이더리움의 동기식 호출보다 더 높은 유연성과 확장성을 제공하며, 응답 대기로 인한 성능 병목 현상을 줄여줍니다. 그러나 동시에 동시성 처리 및 경쟁 조건 문제도 발생시킬 수 있습니다.
메시지 형식 및 구조
TON에서 메시지는 일반적으로 발신자, 수신자, 금액, 메시지 본문 등의 정보를 포함합니다. 메시지 본문은 함수 호출, 데이터 전송 또는 기타 사용자 정의 내용일 수 있습니다. TON이 사용하는 메시지 형식은 유연하게 정의 및 확장 가능하여 다양한 유형의 정보를 서로 다른 계약 간에 효율적으로 전달할 수 있게 합니다.
cell msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(message_body)
.end_cell();
메시지 큐 및 상태 처리
각 계약은 아직 처리되지 않은 메시지를 저장하는 메시지 큐를 유지합니다. 계약은 실행 중 큐에 있는 메시지를 순차적으로 처리합니다. 메시지 처리가 비동기이기 때문에, 메시지를 수신하기 전까지 계약의 상태는 즉시 업데이트되지 않습니다.
비동기 메시지 전달의 장점
-
효율적인 샤딩 메커니즘: TON의 비동기 메커니즘은 샤딩 설계와 높은 수준으로 결합됩니다. 각 샤드는 계약의 메시지와 상태 변화를 독립적으로 처리하여, 샤드 간 동기 통신으로 인한 지연 문제를 피합니다. 이러한 설계는 전체 네트워크의 처리량과 확장성을 향상시킵니다.
-
자원 소모 감소: 비동기 메시지는 즉각적인 응답을 요구하지 않기 때문에, TON의 계약 실행은 여러 블록에 걸쳐 완료될 수 있어 단일 블록 내 자원의 과도한 소모를 방지합니다. 이를 통해 TON은 더욱 복잡하고 자원 집약적인 스마트 계약을 지원할 수 있습니다.
-
오류 허용성과 신뢰성: 비동기 메시지 전달 메커니즘은 시스템의 오류 허용성을 높입니다. 예를 들어, 어떤 계약이 자원 제한이나 기타 이유로 메시지에 즉시 응답하지 못하더라도, 송신 측은 여전히 다른 로직을 계속 처리할 수 있으므로, 시스템이 단일 계약의 지연으로 인해 멈추지 않습니다.
비동기 계약 설계의 도전 과제
-
상태 일관성 문제: 메시지 전달이 비동기이기 때문에, 계약의 상태는 서로 다른 시점에 서로 다른 메시지를 수신할 수 있으며, 이로 인해 개발자는 상태 일관성 문제에 특히 주의해야 합니다. 계약 설계 시에는 다양한 메시지 순서가 초래할 수 있는 상태 변화를 고려하여, 어떤 상황에서도 시스템이 일관성을 유지할 수 있도록 해야 합니다.
-
경쟁 조건 및 방지: 비동기 메시지 처리는 잠재적인 경쟁 조건 문제를 발생시킵니다. 여러 메시지가 동시에 계약 상태를 수정하려 할 수 있으므로, 개발자는 적절한 락 메커니즘 또는 트랜잭션 작업을 도입하여 상태 충돌을 방지해야 합니다.
-
보안 고려사항: 비동기 계약은 크로스-계약 통신 시 중간자 공격(man-in-the-middle attack)이나 재생 공격(replay attack)에 쉽게 노출될 수 있습니다. 따라서 비동기 계약 설계 시 이러한 잠재적 보안 리스크를 반드시 고려하고, 이를 방지하기 위한 조치(예: 타임스탬프, 난수, 다중 서명 등 사용)를 취해야 합니다.
원장 모델
Ton(The Open Network)은 블록체인 인프라를 설계할 때 독특한 계정 추상화(account abstraction) 및 원장 모델을 채택했습니다. 이 모델의 유연성은 계정의 상태 처리, 메시지 전달 및 계약 실행 방식에 반영되어 있습니다.
계정 추상화
TON의 계정 모델은 계약 기반 추상화를 사용하며, 각 계정은 하나의 계약처럼 간주됩니다. 이는 이더리움의 계정 추상화 모델과 유사한 점이 있지만, 더욱 유연하고 보편적입니다. TON에서 계정은 자산을 보유하는 컨테이너일 뿐 아니라, 계약 코드와 상태 데이터를 포함합니다. 각 계정은 코드(Code), 데이터(Data), 메시지 처리(Message Handling) 로직으로 구성됩니다.
계정 구조: 각 TON 계정은 고유한 주소를 가지며, 이 주소는 계정 코드의 해시값, 배포 시 초기 데이터 및 기타 몇 가지 매개변수의 조합으로 생성됩니다. 즉, 동일한 코드와 초기 데이터라도 서로 다른 환경(예: 다른 블록체인 또는 샤드)에 배포되면 다른 주소가 생성될 수 있다는 의미입니다.
유연성: 각 계정이 자체 계약 코드를 실행할 수 있기 때문에, TON 계정은 매우 복잡한 로직을 구현할 수 있습니다. 계정은 단순한 잔액 보유자가 아니라, 복잡한 상태 전이, 계정 간 메시지 통신, 특정 조건 기반 자동화 작업 등을 처리할 수 있습니다. 이는 TON의 계정 모델이 기존 블록체인의 계정 모델보다 확장성과 유연성이 뛰어나다는 것을 의미합니다.
원장 구조
TON의 원장 구조는 대규모 동시성 트랜잭션을 효율적으로 처리하도록 설계되었으며, 비동기 메시지 전달과 다중 샤딩 작업을 지원합니다. 각 계정의 상태는 Merkle 트리 구조에 저장되어, TON 원장이 효율적인 상태 검증 기능을 갖도록 합니다.
상태 저장
계정의 상태 정보는 영속 저장소에 저장되며, Merkle 트리를 통해 조직되어 상태의 무결성과 보안성을 보장합니다. 이 설계는 특히 샤드 간 트랜잭션 시나리오에서 효율적인 상태 조회 및 검증을 지원합니다.
계정 또는 스마트 계약 상태는 일반적으로 다음 내용을 포함합니다:
-
기초 통화 잔액
-
기타 통화 잔액
-
스마트 계약 코드(또는 해당 해시)
-
스마트 계약의 영속 데이터(또는 해당 Merkle 해시)
-
영속 저장 공간 수 및 사용된 원시 바이트 수에 대한 통계 정보
-
통화 전송 및 이 계정에서 메시지 전송을 위한 최근 지불 시간(실제로는 메인체인 블록 번호)
-
통화 이체 및 이 계정에서 메시지 전송에 필요한 공개키(선택 사항; 기본적으로 account_id 자체와 동일). 일부 경우, 비트코인 트랜잭션 출력에서 하는 것처럼, 여기에 더 복잡한 서명 확인 코드를 위치시킬 수 있으며, 이 경우 account_id는 해당 코드의 해시값과 동일해집니다.
모든 정보가 모든 계정에 반드시 필요한 것은 아닙니다. 예를 들어, 스마트 계약 코드는 스마트 계약에만 적용되며 "단순" 계정에는 필요하지 않습니다. 또한, 모든 계정은 주요 통화(예: 기본 워킹체인의 메인체인 및 샤드체인의 Gram)에 대해 0이 아닌 잔액을 가져야 하지만, 다른 통화의 잔액은 0일 수 있습니다. 사용하지 않는 데이터를 보관하지 않기 위해, 워킹체인 생성 시에는 다양한 마커 바이트를 사용하여 서로 다른 '생성자(constructor)'를 구분하는 sum-product 유형이 정의됩니다. 결국, 계정 상태 자체는 TVM 영속 저장소의 셀 집합으로 저장됩니다.
메시지 전달 및 처리
TON의 원장 구조는 비동기 메시지 전달을 내장 지원하며, 각 계정은 수신한 메시지를 독립적으로 처리하고 상태를 업데이트할 수 있습니다. 이와 같은 비동기 메시지 메커니즘은 복잡한 상호작용을 가능하게 하며, 특정 작업의 지연으로 인해 다른 계정의 정상 작동이 영향을 받지 않도록 합니다.
Gas 모델
Ton(The Open Network) 블록체인은 독특한 Gas 요금 모델을 통해 스마트 계약 실행 효율성을 크게 최적화합니다. Gas 모델은 블록체인에서 스마트 계약 실행 시 소모되는 자원을 측정하고 제한하는 데 사용됩니다. 이더리움과 같은 기존 블록체인의 Gas 모델과 비교하면, TON의 모델은 더욱 복잡하고 효율적이며, 계약 실행 과정에서의 자원 소모를 더 정밀하게 관리할 수 있습니다.
세분화된 Gas 소모 측정
TON의 Gas 모델은 스마트 계약이 실행 중 소모하는 계산 자원, 저장 작업, 메시지 전달 비용을 정확하게 측정할 수 있습니다. 계산, 저장, 메시지 전달 등 자원에 대한 세분화된 측정을 통해, TON의 Gas 모델은 과도하게 복잡한 작업이 과도한 자원을 차지하는 것을 방지합니다. Gas 소모를 제한함으로써, TON은 네트워크의 각 노드가 계산 자원을 공평하게 분배받도록 보장하며, 단일 계약이나 작업이 네트워크 자원을 과도하게 소모하는 것을 막습니다.
병렬 처리 및 Gas 최적화
TON은 스마트 계약의 병렬 처리를 지원하여, 여러 계약이 서로 다른 샤드에서 동시에 실행되면서 서로를 차단하지 않도록 합니다. 이러한 설계 하에서, TON의 Gas 모델은 병렬 실행 및 샤딩 메커니즘과 밀접하게 통합되어, 여러 샤드에서 계약을 병렬 처리함으로써 Gas의 계산과 지급을 서로 다른 노드와 체인에 분산시켜 네트워크 혼잡을 방지하면서 자원 활용도를 극대화합니다.
동적 Gas 조정 메커니즘
TON의 Gas 모델은 동적 조정 메커니즘을 포함하여, 네트워크의 실시간 부하 상황에 따라 Gas 요금을 조정할 수 있도록 합니다. 즉, 네트워크 부하가 낮을 때 사용자는 낮은 Gas 요금으로 계약을 실행할 수 있어, 저부하 시간대에 작업을 수행하도록 유도함으로써 네트워크 자원 사용을 균형 있게 만듭니다. 이 메커니즘은 사용자 경험을 향상시킬 뿐 아니라, 시장 기반 방식으로 자원 사용의 피크를 제어합니다.
TON 스마트 계약에서 간과되기 쉬운 취약점
저희가 이전에 발표한 TON 보안 분석 기사에서는 TON 생태계의 일반적인 보안 취약점에 대해 이미 자세히 설명하였습니다. 아래 표를 참고하시기 바랍니다:


본 문서에서는 저희 팀이 정리한 TON 계약에서 자주 간과되기 쉬운 취약점들을 중심으로 소개합니다:
(1) 코드 가독성 최적화
TON 스마트 계약에서는 숫자를 사용하여 메시지 전송 관련 데이터를 저장하는 경우가 많습니다. 아래 코드 예시에서 볼 수 있듯이, 여러 번 숫자를 사용하여 해당 식별자와 데이터 저장 길이를 나타내는데, 이는 코드의 가독성과 유지보수성을 크게 저하시킵니다. 다른 개발자가 이러한 코드를 읽을 때 숫자들의 의미와 용도를 이해하기 어렵습니다. 코드의 가독성과 유지보수성을 높이기 위해, 중요한 숫자 값들은 이름이 붙은 상수로 정의하는 것이 좋습니다. 예를 들어, 0x18은 NON_BOUNCEABLE으로 정의하는 것이 바람직합니다.
check_std_addr(address);var msg = begin_cell() store_uint(0x18, 6) ;; nobounce store_slice(address) store_coins(amount) store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) end_cell();send_raw_message(msg, 1);
또한, 계약 조건 판단 시 나오는 오류 메시지 역시 오류 코드를 변수로 대체하는 것이 좋습니다.
throw_unless(705, equal_slices(owner_address, sender_address));
(2) end_parse()를 사용하여 데이터 무결성 보장
TON 계약에서 데이터 파싱은 고정된 순서를 따르며, 원시 데이터에서 점진적으로 특정 유형의 데이터를 로드합니다. 이러한 파싱 방식은 데이터의 일관성과 정확성을 보장합니다. 아래 예시를 참조하세요:
() load_data() impure {
slice ds = get_data().begin_parse();
storage::owner = ds~load_msg_addr();
storage::amount = ds~load_uint(256);
storage::data = ds~load_ref();
storage::api_data = ds~load_ref();
ds.end_parse();
}
여기서 end_parse()는 데이터 슬라이스(slice)가 비어 있는지 확인하는 데 사용되며, 슬라이스가 비어 있지 않으면 함수는 예외를 발생시킵니다. 이를 통해 데이터 형식과 내용이 예상과 일치하는지 확인할 수 있습니다. 만약 end_parse() 함수가 데이터 슬라이스에 여전히 남아있는 데이터가 있음을 발견한다면, 이는 데이터 파싱이 예상대로 완전히 이루어지지 않았거나 데이터 형식에 문제가 있음을 나타냅니다. 따라서 end_parse()를 호출함으로써 파싱 과정 중 데이터 누락이나 이상 여부를 점검할 수 있습니다.
(3) 데이터 기록 및 저장 유형 불일치로 인한 예외
여기서 주의해야 할 점은 int와 uint의 저장 및 로드 유형 매칭입니다. 아래 코드 예시에서, 데이터 저장 시 store_int()를 사용하여 int형 값 -42를 저장했지만, 이를 load_uint()로 로드하고 있습니다. 이 경우 예외가 발생할 수 있습니다.
() Test_Fuction() {
var cell = begin_cell();
cell = cell.store_int(-42, 32);
var my_cell = cell.end_cell();
slice s = my_cell.begin_parse();
var result = s.load_uint(32);
}
(4) inline_ref 및 inline 수식어의 적절한 사용
먼저, inline_ref 및 inline 수식어의 차이점을 설명하겠습니다:
lInline: inline 수식어가 붙은 함수는 호출될 때마다 코드가 직접 호출 위치에 삽입됩니다. 즉, 매번 함수를 호출할 때 함수의 실제 코드가 호출 위치에 복사되며, 일반 함수처럼 함수 본문으로 점프하여 실행하는 방식이 아닙니다.
linline_ref: inline_ref 수식어가 붙은 함수는 코드가 독립된 셀(cell)에 저장됩니다. 함수를 호출할 때마다 TVM은 CALLREF 명령어를 통해 셀에 저장된 코드를 실행하며, 호출 위치에 함수 코드를 삽입하지 않습니다.
따라서 inline 수식어는 간단한 함수에 적합하여 함수 호출 오버헤드를 줄일 수 있지만, 계약 코드의 중복을 초래할 수 있습니다. 반면 inline_ref 수식어는 보다 복잡하거나 여러 번 호출되는 함수에 적합하며, 함수 코드를 별도의 셀에 저장함으로써 효율성을 높이고 코드 중복을 방지합니다. 요약하자면, 함수가 크거나 여러 곳에서 호출되는 경우에는 inline_ref 사용을 권장하며, 그 외에는 inline 사용을 권장합니다.
(5) 올바른 워킹체인 확인
TON은 최대 2^32개의 워킹체인 생성을 허용하며, 각 워킹체인은 최대 2^60개의 샤드로 세분화될 수 있습니다. 현재는 2개의 워킹체인만 존재합니다: 메인체인(-1)과 기본체인(0). 계약 내에서 목적지 주소를 계산할 때는 반드시 목적지 주소가 속한 체인 ID를 명확히 지정하여, 생성된 지갑 주소가 올바른 워킹체인에 위치하도록 해야 합니다. 잘못된 주소 생성을 방지하기 위해 force_chain()을 사용하여 체인 ID를 강제 지정하는 것이 좋습니다.
(6) 오류 코드 충돌 방지
계약 설계 시 규범성과 혼란 방지를 위해 오류 코드 관리는 매우 중요합니다. TON 스마트 계약의 경우, 우선 각 오류 코드가 계약 내에서 유일해야 하며, 동일 계약 내에서 중복된 오류 코드를 정의하지 않아야 오류 코드의 혼란과 정보의 불명확성을 방지할 수 있습니다. 또한 TON 플랫폼이나 하위 시스템이 이미 정의한 표준 오류 코드와 충돌해서는 안 됩니다. 예를 들어, 오류 코드 333은 체인 ID 불일치를 의미합니다. 따라서 계약의 오류 코드는 400부터 1000 사이로 설정하는 것이 좋습니다.
(7) 작업 완료 후 데이터 저장 및 return() 호출
TON 스마트 계약에서 메시지 처리는 op-code에 따라 다른 로직을 선택합니다. 해당 비즈니스 로직을 완료한 후에는 두 가지 작업을 추가로 수행해야 합니다. 첫째, 데이터 변경이 포함되는 경우 반드시 save_data()를 호출하여 데이터가 저장되도록 해야 하며, 그렇지 않으면 변경 사항이 무효화됩니다. 둘째, 반드시 return()을 호출하여 해당 작업이 완료되었음을 표시해야 하며, 그렇지 않으면 throw(0xffff) 예외가 발생합니다.
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
int flags = cs~load_uint(4);
if (flags & 1) {
;; ignore all bounced messages
return ();
}
slice sender_address = cs~load_msg_addr();
load_data();
int op = in_msg_body~load_op();
if ((op == op::op_1())) {
handle_op1();
save_data();
return ();
}
if ((op == op::op_2())) {
handle_op2();
save_data();
return ();
}
if ((op == op::op_3())) {
handle_op3();
save_data();
return ();
}
throw(0xffff);
}
요약하면, TON 블록체인은 혁신적인 아키텍처와 유연한 개발 환경 덕분에 점점 더 탈중앙화 애플리케이션 개발자의 이상적인 플랫폼으로 자리매김하고 있습니다. 그러나 스마트 계약이 TON 생태계에서 점점 더 중요한 역할을 하면서 계약 보안 문제 또한 간과할 수 없습니다. 개발자는 TON 생태계의 특성을 깊이 이해하고, 최선의 실천 방법을 엄격히 준수하며, 보안 감사를 강화하여 계약의 안정성과 보안성을 보장해야 합니다. 오직 이렇게 함으로써만 TON 플랫폼의 장점을 최대한 발휘하고, 더욱 안전하고 신뢰할 수 있는 탈중앙화 애플리케이션을 구축하여 전체 생태계의 건강한 발전을 지켜낼 수 있습니다.
현재 TON 생태계는 빠르게 성장하며 많은 자금과 활성 사용자를 끌어들이고 있습니다. 그러나 이에 따른 보안 문제 또한 간과할 수 없습니다.
TechFlow 공식 커뮤니티에 오신 것을 환영합니다
Telegram 구독 그룹:https://t.me/TechFlowDaily
트위터 공식 계정:https://x.com/TechFlowPost
트위터 영어 계정:https://x.com/BlockFlow_News














