⚠️
Disclaimer: 본 보고서의 내용은 작성자의 의견을 반영하고 정보 제공만을 목적으로 하며, 토큰을 구매 또는 판매하거나 프로토콜을 사용하도록 권장하는 목적으로 작성되지 않았습니다. 이 보고서에 포함된 어떠한 내용도 투자 조언이 아니며, 투자 조언으로 해석되어서도 안됩니다.

1. 들어가며

클래러티(Clarity)는 비트코인을 기반으로 생태계를 빠르게 확장하고 있는 스택스(Stacks) 체인 위에서 스마트 컨트랙트를 작성할 수 있도록 제공되는 새로운 언어입니다.

Clairty 단어에서 유추해 볼 수 있듯이 보다 명료함을 지향하는 언어로 Type 체크에 Strict 하며, 개발자가 실수하지 않도록 메커니즘이 견고하게 설계되어 있습니다.

Clarity 언어 자체에서 유용하게 제공되는 많은 function들은 일반적인 EVM 기반의 컨트랙트 언어에서 보이는 불필요한 보일러플레이트 코드(Boilerplate code)를 줄여주기도 합니다.

Fungible Token 및 Non Fungible Token 에 관련된 표준은 각 SIP-010, SIP-090 으로 문서화 되어 있으며 Stacks Foundation 에 의해서오픈소스 커뮤니티로 운영됩니다.

최근에는 GitHub 에서 공식 프로그래밍 언어로 인정되었고 현재 2,500개 이상의 Clarity 컨트랙트가 스택스 블록체인 상에 배포되었으며, GitHub 에서는 200개 이상의 Clarity 레포지토리(repository)가 생성되었습니다.

개발자 커뮤니티는 빠른 속도로 확장되고 있으며, 앞으로의 성장이 더욱 기대되는 스택스 생태계입니다.

이번 아티클에서는 독자가 이더리움의 솔리디티(solidity) 컨트랙트를 어느 정도 이해하고 있다고 가정합니다.

이더리움의 Non Fungible Token 표준 스펙인 EIP-721 과 비교하여 Clarity 에서는 NFT를 개발할 때 어떠한 점을 유의해야 하는지에 대해서, 제가 개발하면서 겪었던 경험과 개인적인 생각을 공유합니다.

2. Clarity 와 Solidity

2.1. NFT 전송과 위임

Clarity의 Non Fungible Token 에 대한 스펙은 SIP-009 이며 아래와 같습니다.

(define-trait nft-trait
(
(get-last-token-id () (response uint uint))
(get-token-uri (uint) (response (optional (string-ascii 256)) uint))
(get-owner (uint) (response (optional principal) uint))
(transfer (uint principal principal) (response bool uint))
)
)

Solidity의 Non Fungible Token에 대한 스펙은 EIP-721 이며 아래와 같습니다.

interface ERC721 {
...
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

이 둘의 차이는 메소드(method) 명에서 볼 수 있듯이 NFT의 소유권을 양도할 수 있는 권한이 있는지 혹은 없는지에 대한 차이가 있습니다.

EIP-721은 NFT를 소유자 대신 대리자(delegator)가 승인(approve)된 주소로 위임하는 기능이 정의되어 있습니다.

Clarity는 현재 위와 같은 위임 기능이 없기 때문에 컨트랙트 개발자에게 이 기능을 구현하도록 위임하고 있으며 승인 메커니즘이 필요하지 않습니다.

따라서, 만약 누군가에게 NFT를 위임하고 전송할 수 있도록 하기 위해서는 내부의 맵(map) 자료구조를 활용하여 특정 주소를 화이트리스트(whitelist)로 등록하고, 가져갈 수 있도록 별도의 로직 구현이 필요합니다.

혹은 에스크로(escrow) 역할을 하는 외부 컨트랙트로 NFT를 전송하고 관리할 수 있도록 구현할 수 있습니다. Clarity 개발자는 NFT 관련 컨트랙트를 개발할 때 이러한 언어적인 제약을 염두에 두고 개발해야 할 것입니다.

2.2. NFT 전송 로직에서의 Validation

우리는 앞서, Clarity 에서는 NFT 인터페이스에서 위임 기능은 현재 제공하지 않고 있다는 것을 확인했습니다.

그럼 실제 NFT 전송 로직을 작성할 때 주의해야 할 점에 대해서 조금 더 이야기해 보겠습니다.

Clarity 에서는 대표적인 객체 지향 언어인 자바(java)와 같이 인터페이스를 정의할 수 있습니다. Clarity 에서는 이를 트레잇(trait) 용어로 정의하고 있으며, 우리가 일반적인 인터페이스를 알고 있는 것처럼 trait 에서 정의된 함수명과 argument와 return 값을 구현하도록 정의합니다.

(transfer (uint principal principal) (response bool uint))

위 trait 에서 정의된 함수의 argument는 아래와 같습니다.

  1. NFT의 index 값
  2. NFT의 owner 주소
  3. NFT를 받는 recipient 주소

여기서 2번째에 해당되는 NFT의 owner 주소는 Clarity 언어에서 강제하고 있지는 않습니다. 선택적으로 넣을 수 있는 주소가 됩니다.

물론, 실제 NFT의 owner가 아닌 다른 주소로 실행하게 되면 실제 소유자의 주소와 일치하지 않으므로 전송에는 실패하게 됩니다.

앞서 우리가 확인한 것처럼 Clarity에서는 위임 기능을 제공하고 있지 때문에 전송 주체는 NFT 소유자만 가능합니다. 하지만, 해당 argument를 선택적으로 받을 수 있는 것은 모호함(ambiguity)을 만들게 됩니다. 모호함은 언제나 그렇듯 소프트웨어에 문제점을 일으키는 주된 요인입니다.

(nft-transfer? {nft-asset-class} {nft-asset-identifier} {nft-sender} {nft-recipient})

Clarity 에서 내장된 NFT 전송 메소드는 위와 같은데, 여기서도 마찬가지로 NFT sender 즉, 소유자의 주소를 argument로 제공해야 합니다.

그러나 여기서도 NFT 소유자와 tx-sender 사이의 관계가 약간의 모호함이 발견됩니다.

tx-sender는 Clarity에서 제공하는 트랜잭션을 발행하는 주소 또는 서명하는 주소가 할당되는 변수입니다

왜 모호함이 있는지, 조금 더 설명해 보겠습니다.

컨트랙트에서 다른 컨트랙트를 호출할 때는 tx-sender가 호출된 컨트랙트의 주소로 변경이 될 수 있습니다. 따라서, 외부의 컨트랙트가 NFT와 관련된 권한을 남용할 수 있으며 소유자의 허가 없이 자산을 이동하거나 소유자가 실행할 수 있는 권한을 훔칠 수 있는 여지를 남길 수 있습니다.

이를 막기 위해서는 아래와 같이 실제 NFT 민팅을 수행하는 컨트랙트 메소드에서 tx-sender가 NFT를 전송하는 사람과 일치하는지 validation을 하는 것이 좋습니다. 하지만 이에 대해서 Clarity 에서는 강제하고 있지 않으므로 Clarity 코드를 작성하는 개발자는 이 점을 유의해야 할 필요가 있습니다.

(define-public (transfer (id uint) (sender principal) (recipient principal))
(begin
(asserts! (is-eq tx-sender sender) ERR-NOT-AUTHORIZED)
(match (nft-transfer? nft-asset-class id sender recipient)
success (ok success)
error (err error)
)
)
)

사소한 부분이라고 생각할 수 있지만, 스마트 컨트랙트는 한번 배포가 되면 변경할 수 없다는 점을 우리는 다시 상기할 필요가 있습니다.

사소한 인터페이스의 모호함이 추후에는 돌이킬 수 없는 큰 장애를 만들 수 있으므로 컨트랙트 작성 시에는 블록체인 네트워크에 배포하기 전에 Validation과 Test가 가장 중요합니다.

2.3. 반복문의 제약

흔히 말하는 Web 2.0 세계에서의 개발자들에게 반복문(iteration)은 조건문(conditional statements)과 같이 필수적인 문법일 것입니다.

하지만 Web 3.0 세계에서 가장 위험한 문법이 재귀(recursion)와 반복(iteration) 입니다.

제한된 하나의 블록에 트랜잭션을 포함해서 노드를 구성하고 브로드캐스팅 해야 하는 블록체인 네트워크에서 특정 컨트랙트 내의 반복문을 통해서 보관되는 데이터(data) 혹은 연산(operation)을 기하급수적으로 증가시킬 수 있으므로 쉽게 네트워크 과부하로 이어질 수 있기 때문입니다.

먼저 Solidity의 반복문을 살펴보겠습니다. 간단하게 작성한 2개 이상의 NFT 민팅(Minting) 할 수 있는 메소드 입니다.

function mintTokens(uint numberOfTokens) public payable {
require(saleIsActive, "Sale must be active to mint");
require(initPrice.mul(numberOfTokens) <= msg.value, "ETH value sent is not correct");
require(totalSupply().add(numberOfTokens) <= MAX_NFT_AMOUNT, "Purchase would exceed max supply"); for(uint i = 0; i < numberOfTokens; i++) {
mint(...);
}
}

위 코드에서 볼 수 있듯이, Solidity 에서는 for-loop 문법을 제공하고 있으므로, 특정 NFT 개수만큼 for-loop 을 통해서 민팅(Minting) 할 수 있습니다.

Solidity 에서 이러한 반복문의 사용이 가능한 이유는 이더리움의 Gas 정책을 통해서 보안 메커니즘을 구성하고 있기 때문입니다.

이더리움에서 Gas량은 Opcode(bytecode) 양에 따라 결정이 되며 메모리 사용량과 제어문과 같은 유닛(unit) 당 Gas 가격이 다릅니다.

또한 트랜잭션 실행 시, Gas 가격과 트랜잭션에서 소모될 수 있는 Gas 상한값(gas limit)을 같이 포함하도록 하기 때문에 트랜잭션에 소모되는 Gas양이 트랜잭션에 제출된 Gas 상한선을 넘게 되면 트랜잭션은 자동적으로 실패하게끔 설계되어 있습니다.

이에 반면, Clarity 의 경우는 애초에 악의적인 반복문을 통한 네트워크 마비를 애초에 방지하기 위해서 반복문과 관련된 문법을 제공하고 있지 않습니다.

Clarity 에서는 이러한 언어적인 제약으로써 Infinite Loop를 하지 못하도록 제한하고 있습니다. 다만 우회적으로 특정 리스트 내에서 Loop를 실행할 수 있도록 할 수 있습니다.

Functional Programming 에서 주로 등장하는 fold 함수를 통해서 리스트 내에 순회가 가능합니다.

(define-public (mint-ten) (mint (list true true true true true true true true true true)))(define-private (mint (iterable (list 30 bool)))
...
(begin
(mint-many iterable)
)
)(define-private (mint-many (iterable (list 30 bool)))
(let
(
...
(tail-id (fold mint-many-iter iterable last-nft-id))
...
)
...
(ok tail-id)
)
)(define-private (mint-many-iter (ignore bool) (next-id uint))
(if (<= next-id (var-get mint-limit))
(begin
(unwrap! (nft-mint? nft-name next-id tx-sender) next-id)
(+ next-id u1)
)
next-id
)
)

fold 함수는 리스트의 왼쪽부터 오른쪽 끝까지 현재의 계산값을 시작으로 이전의 출력을 재귀적으로 연속해서 적용하여 마지막으로 반환된 값을 반환하는 함수입니다.

따라서, 우리가 의도했던 반복문을 재귀적으로 실행할 수 있습니다. 하지만 제한된 길이의 리스트에서만 적용이 가능하므로 무한 루프(infinite loop)의 가능성을 배제할 수 있습니다.


참고자료