이슈

커뮤니티

2024.11.27

Web3 보안 개발 실천: Clarity 모범 사례 요약


최근 CertiK 팀은 비트코인 생태계와 그 발전에 대해 심층 연구를 진행했습니다. 또한, 여러 비트코인 프로젝트와 다양한 프로그래밍 언어로 작성된 스마트 컨트랙트의 보안 감사도 수행하였으며, 여기에는 OKX의 BRC-20 지갑과 MVC DAO의 sCrypt 스마트 컨트랙트 구현이 포함됩니다.

현재 연구는 Clarity에 초점을 맞추고 있습니다. CertiK 팀은 여러 Clarity 취약점 보상 프로젝트를 성공적으로 완료한 후, 이와 관련된 보안 문제와 일반적인 실천에 대한 인사이트를 얻었습니다. 본 블로그에서는 이러한 인사이트와 경험을 공유하여 생태계 구축자들에게 도움이 되고자 합니다.

Clarity는 Hiro PBC, Algorand 및 기타 이해관계자들이 공동 개발한 스마트 컨트랙트 언어로, 현재 Stacks 체인(비트코인 사이드체인)에서 사용되고 있습니다. Clarity의 주요 목표는 높은 예측 가능성과 안전성을 제공하여 스마트 컨트랙트가 예상대로 실행되고 예기치 않은 부작용이 발생하지 않도록 하는 것입니다.

아래, Clarity 스마트 컨트랙트의 개념과 Clarity 프로그래밍의 모범 사례 및 보안 점검 목록에 대해 살펴보겠습니다.

# Clarity 언어

Clarity의 설계는 스마트 컨트랙트 엔지니어링에서 발생하는 취약점에 대한 심층 분석, 특히 Solidity에서 관찰된 취약점 연구에 기반하고 있습니다. Clarity의 주요 특징은 다음과 같습니다:

- 해석 가능한 언어: 명확하고 직관적인 표현을 보장합니다.

- 판별 가능한 속성: 예측 가능한 결과와 제한된 실행을 보장합니다.

- 보안 조치: 재진입 공격, 오버플로우 및 언더플로우를 방지하여 컨트랙트의 무결성을 유지합니다.

- 맞춤형 토큰 지원: 개발 프로세스를 간소화하고 토큰 생성 및 관리가 용이합니다.

- 거래 후 조건: 실행 후 상태 변화를 검증하여 보안을 강화합니다.

Clarity의 독특한 점은 LISP 언어에서 영감을 받았다는 것입니다. LISP는 기호 정보를 처리하는 간결성과 강력한 기능으로 유명합니다. Clarity에서는 모든 것이 "리스트 안의 리스트" 또는 "표현식 안의 표현식" 형태로 표현됩니다. 이러한 중첩 구조는 Clarity의 핵심 특징으로, 높은 표현력과 유연성을 제공합니다. 함수 정의, 변수 선언, 함수 매개변수는 모두 괄호 안에 포함되어 언어의 문법 일관성을 강조합니다.

다음은 간단한 Clarity 함수를 정의하는 예시입니다:



이러한 중첩 표현식을 이해하고 활용함으로써 개발자는 Stacks 블록체인 기능 요구에 부합하는 안전하고 효율적인 스마트 컨트랙트를 만들 수 있습니다. 이 접근 방식은 가독성을 향상시키고 컨트랙트의 결정성과 예측 가능성을 보장하여 탈중앙화 애플리케이션의 안전성과 신뢰성을 유지하는 데 중요한 역할을 합니다.

# Clarity와 Solidity의 차이

1.해석형과 컴파일형

Clarity: Clarity는 해석형 언어로, 소스 코드가 직접 Stacks 블록체인에 전송되어 블록체인에서 실행됩니다. 따라서 바이트코드로 컴파일할 필요가 없습니다.

Solidity: Solidity 코드는 먼저 바이트코드로 컴파일된 후, 이 바이트코드가 블록체인에 배포됩니다. EVM(이더리움 가상 머신)은 이 바이트코드를 검증하고 해당 작업 코드를 실행합니다.

2. 동적 스케줄링 없음

Clarity는 동적 스케줄링을 지원하지 않으며, Turing 완전성이 없습니다. 즉, 실행할 함수가 미리 정해져 있습니다. 이러한 특성은 실행 모델을 단순화하고 재진입 공격을 방지하여 Clarity의 보안을 강화합니다.



# Clarity 스마트 컨트랙트 보안

보안은 DeFi 분야에서 항상 최우선 과제이며, 특히 Stacks 네트워크가 핵심 역할을 하는 비트코인 DeFi 생태계에서 더욱 중요합니다. 2024년 8월 기준으로 Stacks 생태계의 가상자산 예치 총액(TVL)는 약 8000만 달러에 달하고 있어, 강력한 보안 조치가 필수적입니다.

현재까지 Stacks에서는 여러 차례 보안 사건이 발생하여 200만 달러 이상의 손실이 발생했습니다. 이러한 사건들은 Clarity 스마트 컨트랙트에 대한 보안 감사의 필요성을 더욱 강조합니다.

1. 보안 사건 사례

2024년 4월 11일, Stacks 네트워크의 대출 프로토콜 Zest Protocol(비트코인 L2)은 중대한 보안 취약점 공격을 받았습니다. 공격의 목표는 프로토콜의 대출 풀(Borrow Pool)로, 이로 인해 약 322,000 STX(약 100만 달러)가 손실되었습니다. 이 사건은 현재까지 비트코인 DeFi 생태계에서 가장 큰 손실을 초래한 사건입니다.


Zest Protocol의 대출 기능은 컨트랙트 pool-borrow.clar에 정의되어 있으며, 사용자가 담보를 제공하여 자산을 빌릴 수 있도록 합니다. 이 기능의 매개변수에는 풀 저장소, 가격 오라클, 빌린 자산, 유동성 제공자 토큰, 담보 자산 목록, 대출 금액, 수수료 계산기, 이자율 모델 및 소유자가 포함됩니다.


(define-public (borrow
(pool-reserve principal)
(oracle <oracle-trait>)
(asset-to-borrow <ft>)
(lp <ft>)
(assets (list 100 { asset: <ft>, lp-token: <ft>, oracle: <oracle-trait> }))
(amount-to-be-borrowed uint)
(fee-calculator principal)
(interest-rate-mode uint)
(owner principal))

pool-borrow-v1-1.clar의 대출 함수


공격자는 assets(자산) 매개변수를 악용했습니다. 이 매개변수는 최대 100종의 자산을 포함할 수 있는 목록으로, 담보로 사용됩니다. 취약점은 컨트랙트가 담보 자산의 고유성을 검증하지 못한 데서 발생했습니다. 구체적으로, 컨트랙트는 자산의 존재성을 확인할 때 중복 여부를 체크하지 않았습니다. 이 실수로 인해 공격자는 동일한 자산을 여러 번 나열하여 담보의 가치를 조작할 수 있었습니다.

유사한 보안 취약점 공격 사례는 다른 프로토콜에서도 발생했습니다. 예를 들어, 2021년 10월 한 공격자가 Arkadiko Swap에서 약 400,000 STX와 740,000 USDA(총약 150만 달러)를 탈취했습니다. 공격자는 Arkadiko Swap 스마트 컨트랙트 코드의 취약점을 이용해 새로운 거래 쌍을 생성할 때 LP 토큰을 올바르게 검증하지 못한 점을 악용했습니다. 이 취약점으로 인해 공격자는 비용 없이 대량의 LP 토큰을 발행할 수 있었고, 이후 STX/USDA 풀에서 기본 자산을 인출하여 해당 풀의 총 가치의 25%에 영향을 미쳤습니다.

2. Clarity 스마트 컨트랙트의 모범 사례 및 체크리스트

우리는 광범위한 연구를 통해 얻은 경험을 바탕으로 Clarity 스마트 컨트랙트 개발자를 위한 모범 사례 및 체크리스트를 작성했습니다. 아래는 주요 사항입니다:

1. -panic 함수 사용 피하기

Clarity 스마트 컨트랙트에서 값을 언팩 할 때, unwrap-panic 및 unwrap-err-panic 같은 함수를 사용하는 것을 피해야 합니다. 이러한 함수가 언팩에 실패하면 런타임 오류로 호출이 중단되지만, 컨트랙트와 상호작용하는 애플리케이션에 의미 있는 정보를 제공하지 않습니다. 대신 unwrap! 와 unwrap-err!를 사용하고 명확한 오류 코드를 추가하는 것이 좋습니다. 이 방법은 오류 처리 능력을 향상시키고 디버깅을 용이하게 하며 스마트 컨트랙트의 강인성을 강화합니다. 구체적인 오류 코드를 사용하면 호출 애플리케이션이 오류를 더 효율적으로 처리하고 상황에 맞는 적절한 조치를 취할 수 있습니다.

2. tx-sender를 이용한 검증 피하기


Clarity 스마트 컨트랙트에서 tx-sender 변수를 남용하여 인증을 수행하는 것은 보안 취약점을 초래할 수 있으며, 이는 Solidity의 SWC-115에 나열된 취약점과 유사합니다. tx-sender 변수는 호출 체인의 발신자를 식별하며, Solidity의 tx.origin과 비슷한 역할을 합니다. tx-sender를 이용한 인증은 피싱 공격을 유발할 수 있으며, 공격자가 사용자를 속여 취약한 컨트랙트에서 인증된 작업을 수행할 수 있습니다.


반면, contract-caller는 현재 호출된 발신자를 나타냅니다. tx-sender를 이용한 인증을 피하고 contract-caller와 같은 더 안전한 대안을 사용함으로써, 개발자는 피싱 공격 및 크로스 사이트 스크립팅 공격의 위험을 줄일 수 있습니다.

3. 모듈화 컨트랙트 설계로 유연성과 미래의 업그레이드 가능성 강화

스마트 컨트랙트는 한번 블록체인에 배포되면 수정할 수 없게 됩니다. 이는 전통적인 애플리케이션 개발과 비교할 때 도전 과제가 되며, 언제든지 업데이트하거나 수정할 수 없음을 의미합니다. 스마트 컨트랙트 개발에서 유연성과 미래의 업그레이드 가능성을 보장하기 위해서는 전략적인 접근이 필요합니다. 컨트랙트가 배포된 이후에는 컨트랙트 코드를 직접 업데이트할 수 있는 방법이 없기 때문입니다.

이러한 도전 과제를 해결하기 위해 개발자는 다음 원칙을 고려해야 합니다:

논리적 분리 유지: 모든 기능을 처리하는 단일 컨트랙트를 만드는 대신, 스마트 컨트랙트를 모듈화하여 더 작고 독립적이며 상호 작용할 수 있는 구성 요소로 나누어야 합니다. 이러한 접근 방식은 컨트랙트를 더 쉽게 관리하고 이해할 수 있게 하며, 전체 시스템에 영향을 주지 않고 개별 구성 요소를 교체하거나 업그레이드할 수 있도록 합니다.

무상태 컨트랙트: 무상태 컨트랙트는 블록체인에 최소한의 데이터를 저장하여 향후 변경의 복잡성과 잠재적 영향을 줄입니다. 상태를 컨트랙트 외부에 보관하고 입력 매개변수로 전달함으로써, 컨트랙트의 상태를 수정하지 않고도 로직을 업데이트할 수 있습니다.

하드코딩 변수 피하기: 값을 컨트랙트 코드에 직접 하드코딩하면 유연성이 떨어지고 향후 업데이트에 방해가 될 수 있습니다. 대신, 중요한 변수를 구성 가능한 매개변수로 정의하여 컨트랙트 함수로 설정하거나 조정할 수 있도록 해야 합니다.

4. 블록 높이 기반의 시간 계산 피하기

Clarity 스마트 컨트랙트에서는 시간에 민감한 계산을 위해 block-height 키워드에 의존하는 것을 피해야 합니다. Stacks 체인의 블록 시간은 네트워크 업그레이드에 따라 변경될 수 있으며, 예를 들어 Nakamoto 버전의 출시가 블록 시간을 단축시킬 수 있습니다. 대신 burn-block-height 키워드를 사용하는 것이 좋습니다. 이 키워드는 기본 비트코인 블록체인의 현재 블록 높이를 반영하여, 비트코인의 블록 시간이 더 안정적이고 변경될 가능성이 낮습니다. 이를 통해 컨트랙트 작업의 정확성과 신뢰성을 높일 수 있습니다. 이러한 접근 방식은 일관성을 유지하고 Stacks 블록 시간의 변동으로 인한 잠재적인 문제를 방지하는 데 기여합니다.

5. 함수에서 반환 값 올바르게 처리하기

Clarity 스마트 컨트랙트를 개발할 때, 함수의 불리언 반환 값을 올바르게 처리하는 것이 중요합니다. 특히 verify-mined()와 같은 함수를 사용할 때 더욱 주의해야 합니다.

이 함수는 세 가지 가능한 값을 반환합니다: (ok true), (ok false) 또는 오류입니다. (ok true)가 반환되면 거래가 지정된 블록에서 성공적으로 채굴되었음을 의미하고, (ok false)가 반환되면 거래가 채굴되지 않았음을 나타냅니다. 오류는 Merkle 증명에 문제가 있음을 뜻합니다.

try!를 사용하여 ok/error를 확인할 때, 응답 유형에 포함된 불리언 값을 검증하지 않는 일반적인 문제가 발생할 수 있습니다. 이러한 실수는 거래가 블록에서 채굴되지 않았음에도(즉, 반환 값이 (ok false)일 때) 함수가 실패하지 않게 만들 수 있습니다. 그 결과, 검증자가 채굴되지 않은 거래에 대해 협력하여 서명하게 되고, 이 거래가 검토 없이 인덱서에 통과될 수 있습니다. 이러한 취약점은 검증되지 않은 미채굴 거래와 잠재적으로 악의적인 거래가 처리되도록 하여 보안 취약점과 시스템 내의 무단 작업을 초래할 수 있습니다.

이러한 리스크를 줄이기 위해, 코드에서 오류를 확인하고 함수가 반환하는 불리언 값을 명확히 검증하는 것이 중요합니다. 이러한 접근 방식은 컨트랙트의 무결성과 안전성을 유지하며, 유효한 채굴 거래만 처리하도록 보장하는 데 도움이 됩니다.

6. Clarity에서 contract-call?를 올바르게 사용하기

Clarity 스마트 컨트랙트를 개발할 때, contract-call? 함수를 사용하여 컨트랙트 간 호출을 정확하게 구현해야 합니다. 이 함수는 호출된 스마트 컨트랙트에서 Response(응답) 유형의 결과를 반환합니다.

contract-call?에는 두 가지 유형이 있습니다:

정적 호출 (Static Call): 호출되는 컨트랙트는 배포 시 체인에서 사용할 수 있는 고정된 컨트랙트입니다. 첫 번째 매개변수는 호출되는 컨트랙트의 본체이며, 그다음에 메서드 이름과 매개변수가 따라옵니다.


동적 호출 (Dynamic Call): 호출되는 컨트랙트를 매개변수로 전달하고, 이를 트레이트 참조(trait reference)로 타입화합니다. 이렇게 함으로써 코드의 유연성과 재사용성이 향상됩니다.

컨트랙트 호출을 처리할 때 다음 제한 사항에 유의해야 합니다:

- 정적 호출에서 호출되는 스마트 컨트랙트는 생성 시 반드시 존재해야 합니다.

- 스마트 컨트랙트의 호출 그래프에는 순환이 존재해서는 안 됩니다. 이는 재귀(및 재진입)를 방지하기 위한 것입니다. 이러한 구조는 호출 그래프의 정적 분석을 통해 감지될 수 있으며, 네트워크에서 거부됩니다.

- contract-call?는 컨트랙트 간 호출에만 사용됩니다. 호출자가 동시에 호출되는 경우, 실행을 시도하면 거래가 중단됩니다.

# 마무리 글

CertiK은 Clarity 스마트 컨트랙트 보안에 대해 심층적인 연구를 진행해 왔습니다. Web3 보안 분야의 선두주자로서 풍부한 경험을 보유한 CertiK은 Clarity 기반의 여러 버그 바운티 프로젝트에서 발견된 취약점을 보고했습니다. 이전의 리스크 분석 보고서는 CertiK 블로그를 통해 확인하실 수 있습니다.

전문가탭의 모든 콘텐츠는 무단 도용, 배포 및 사용을 금합니다. 또한 상기 내용은 재무, 법률적 조언이 아니며, 특정 자산의 매매를 권장하지도 않습니다. 제3자 기고자가 작성한 글의 경우 코인니스의 의견이 반영되는 것은 아니라는 점을 유의하시기 바랍니다.

61542

댓글을 보시려면 로그인을 하셔야 해요

의견을 공유하고 아이디어를 나눠봐요.
로그인
Loading