본문으로 건너뛰기

Vyper ERC-721 컨트랙트 연습

Vyper
erc-721
Python
초급
오리 포메란츠
2021년 4월 1일
38 분 소요

소개

ERC-721 표준은 대체 불가능한 토큰(NFT)의 소유권을 보유하는 데 사용됩니다. ERC-20 토큰은 개별 토큰 간에 차이가 없기 때문에 상품처럼 작동합니다. 이와 대조적으로, ERC-721 토큰은 서로 다른 고양이 만화 (새 탭에서 열림)나 서로 다른 부동산의 소유권과 같이 유사하지만 동일하지 않은 자산을 위해 설계되었습니다.

이 글에서는 Ryuya Nakamura의 ERC-721 컨트랙트 (새 탭에서 열림)를 분석해 보겠습니다. 이 컨트랙트는 Solidity보다 안전하지 않은 코드를 작성하기 어렵게 설계된 Python과 유사한 컨트랙트 언어인 Vyper (새 탭에서 열림)로 작성되었습니다.

컨트랙트

# @dev ERC-721 대체 불가능한 토큰 표준의 구현입니다.
# @author Ryuya Nakamura (@nrryuya)
# 수정된 출처: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

Vyper의 주석은 Python과 마찬가지로 해시(ethereum.ercs)로 시작하여 줄 끝까지 이어집니다. @<keyword>가 포함된 주석은 사람이 읽을 수 있는 문서를 생성하기 위해 NatSpec (새 탭에서 열림)에서 사용됩니다.

from vyper.interfaces import ERC721

implements: ERC721

ERC-721 인터페이스는 Vyper 언어에 내장되어 있습니다. 여기에서 코드 정의를 확인할 수 있습니다 (새 탭에서 열림). 인터페이스 정의는 Vyper가 아닌 Python으로 작성됩니다. 인터페이스는 블록체인 내부뿐만 아니라 외부 클라이언트(Python으로 작성될 수 있음)에서 블록체인으로 트랜잭션을 전송할 때도 사용되기 때문입니다.

첫 번째 줄은 인터페이스를 가져오고(import), 두 번째 줄은 여기서 해당 인터페이스를 구현하고 있음을 지정합니다.

#pragma version >0.3.10
#pragma version >0.3.10

ERC721Receiver 인터페이스

# safeTransferFrom()에 의해 호출되는 컨트랙트를 위한 인터페이스
interface ERC721Receiver:
    def onERC721Received(

ERC-721은 두 가지 유형의 전송을 지원합니다:

  • transferFrom: 발신자가 임의의 목적지 주소를 지정할 수 있으며 전송에 대한 책임을 발신자에게 부여합니다. 즉, 유효하지 않은 주소로 전송할 수 있으며, 이 경우 NFT는 영원히 손실됩니다.
  • safeTransferFrom: 목적지 주소가 컨트랙트인지 확인합니다. 컨트랙트인 경우, ERC-721 컨트랙트는 수신 컨트랙트에게 NFT를 받을 것인지 묻습니다.

safeTransferFrom 요청에 응답하려면 수신 컨트랙트가 ERC721Receiver를 구현해야 합니다.

            _operator: address,
            _from: address,

_from 주소는 토큰의 현재 소유자입니다. _operator 주소는 전송을 요청한 주소입니다(허용량(allowance) 때문에 이 둘은 같지 않을 수 있습니다). 관례적으로 이 컨트랙트의 대부분의 함수 매개변수는 밑줄(_)로 시작합니다.

            _tokenId: uint256,

ERC-721 토큰 ID는 256비트입니다. 일반적으로 토큰이 나타내는 대상의 설명을 해싱하여 생성됩니다.

            _data: Bytes[1024]

요청에는 최대 1024바이트의 사용자 데이터가 포함될 수 있습니다.

        ) -> bytes4: nonpayable

컨트랙트가 실수로 전송을 수락하는 경우를 방지하기 위해 반환 값은 부울이 아니라 특정 4바이트 값인 onERC721Received의 함수 선택자(function selector)입니다. 수신 컨트랙트가 토큰을 수락할 때 자체 상태를 변경할 수 있으므로 이 함수는 nonpayable입니다.

이벤트

이벤트는 블록체인 외부의 사용자와 서버에 이벤트를 알리기 위해 발생(emit)됩니다. 이벤트의 내용은 블록체인 상의 컨트랙트에서 사용할 수 없다는 점에 유의하세요. 세 가지 ERC-721 이벤트는 우리가 가져온 IERC721 인터페이스에 정의되어 있으므로, 이 컨트랙트는 이를 직접 선언하지 않습니다. 아래의 전송 함수에서 볼 수 있듯이 log IERC721.<Event>(...)를 사용하여 이벤트를 발생시킵니다.

Transfer(sender, receiver, token_id)는 NFT 소유권의 변경을 보고합니다. 이는 금액 대신 token_id를 보고한다는 점을 제외하면 ERC-20 Transfer 이벤트와 유사합니다. 아무도 제로 주소를 소유하지 않으므로, 관례적으로 토큰의 생성 및 소각을 보고하는 데 사용합니다. 한 가지 예외는 컨트랙트 생성 시점인데, 이 때는 Transfer를 발생시키지 않고도 임의의 수의 NFT를 생성하고 할당할 수 있습니다.

ERC-721 승인(approval)은 ERC-20 허용량과 유사합니다. 특정 주소가 특정 토큰을 전송하도록 허용되며, 승인된 주소가 설정되거나 재확인될 때마다 Approval(owner, approved, token_id)이 발생합니다. 이는 컨트랙트가 토큰을 수락할 때 응답할 수 있는 메커니즘을 제공합니다. 컨트랙트는 이벤트를 수신(listen)할 수 없으므로, 단순히 토큰을 전송하기만 하면 컨트랙트는 이를 "알지" 못합니다. 이러한 방식으로 소유자는 먼저 승인을 제출한 다음 컨트랙트에 "토큰 X를 전송하도록 승인했으니, ...를 수행해 주세요"라는 요청을 보냅니다. 이는 ERC-721 표준을 ERC-20 표준과 유사하게 만들기 위한 설계상의 선택입니다. ERC-721 토큰은 대체 불가능하므로, 컨트랙트는 토큰의 소유권을 확인하여 특정 토큰을 받았는지 식별할 수도 있습니다.

마지막으로, 소유자에 대해 _운영자(operator)_가 활성화되거나 비활성화될 때 ApprovalForAll(owner, operator, approved)이 발생합니다. 위임장과 유사하게, 특정 유형(특정 컨트랙트에서 관리하는 토큰)의 계정 토큰을 모두 관리할 수 있는 운영자를 두는 것이 유용할 때가 있습니다. 예를 들어, 6개월 동안 연락이 없었는지 확인하고 그렇다면 내 자산을 상속인에게 분배하는 컨트랙트에 이러한 권한을 부여하고 싶을 수 있습니다(상속인 중 한 명이 요청하는 경우에 한하며, 컨트랙트는 트랜잭션에 의해 호출되지 않으면 아무것도 할 수 없습니다). ERC-20에서는 상속 컨트랙트에 높은 허용량을 부여하기만 하면 되지만, ERC-721에서는 토큰이 대체 불가능하기 때문에 이 방법이 작동하지 않습니다. 이것이 그에 상응하는 기능입니다. approved 값은 이벤트가 승인을 위한 것인지, 아니면 승인 철회를 위한 것인지 알려줍니다.

상태 변수

이 변수들은 토큰의 현재 상태, 즉 어떤 토큰을 사용할 수 있고 누가 소유하고 있는지를 포함합니다. 이들 대부분은 HashMap 객체로, 두 유형 사이에 존재하는 단방향 매핑 (새 탭에서 열림)입니다.

# @dev NFT ID에서 이를 소유한 주소로의 매핑입니다.
idToOwner: HashMap[uint256, address]

# @dev NFT ID에서 승인된 주소로의 매핑입니다.
idToApprovals: HashMap[uint256, address]

이더리움에서 사용자와 컨트랙트의 신원은 160비트 주소로 표시됩니다. 이 두 변수는 토큰 ID를 소유자 및 전송하도록 승인된 주소(각각 최대 1개)에 매핑합니다. 이더리움에서 초기화되지 않은 데이터는 항상 0이므로, 소유자나 승인된 전송자가 없는 경우 해당 토큰의 값은 0입니다.

# @dev 소유자 주소에서 소유한 토큰 수로의 매핑입니다.
ownerToNFTokenCount: HashMap[address, uint256]

이 변수는 각 소유자의 토큰 수를 보유합니다. 소유자에서 토큰으로의 매핑은 없으므로, 특정 소유자가 소유한 토큰을 식별하는 유일한 방법은 블록체인의 이벤트 기록을 되돌아보고 적절한 Transfer 이벤트를 확인하는 것뿐입니다. 이 변수를 사용하면 모든 NFT를 언제 확보했는지 알 수 있으므로 시간을 더 거슬러 올라가 확인할 필요가 없습니다.

이 알고리즘은 사용자 인터페이스와 외부 서버에서만 작동한다는 점에 유의하세요. 블록체인 자체에서 실행되는 코드는 과거 이벤트를 읽을 수 없습니다.

# @dev 소유자 주소에서 운영자 주소 매핑으로의 매핑입니다.
ownerToOperators: HashMap[address, HashMap[address, bool]]

계정에는 둘 이상의 운영자가 있을 수 있습니다. 단순한 HashMap은 각 키가 단일 값으로 이어지기 때문에 이들을 추적하기에 충분하지 않습니다. 대신 HashMap[address, bool]을 값으로 사용할 수 있습니다. 기본적으로 각 주소의 값은 False이며, 이는 운영자가 아님을 의미합니다. 필요에 따라 값을 True로 설정할 수 있습니다.

# @dev 토큰을 발행할 수 있는 발행자의 주소입니다.
minter: address

새로운 토큰은 어떻게든 생성되어야 합니다. 이 컨트랙트에서는 이를 수행할 수 있는 단일 엔터티인 minter가 있습니다. 예를 들어 게임의 경우 이것으로 충분할 가능성이 높습니다. 다른 목적을 위해서는 더 복잡한 비즈니스 로직을 생성해야 할 수도 있습니다.

# @dev 지원되는 ERC165 인터페이스 ID의 정적 목록입니다.
SUPPORTED_INTERFACES: constant(bytes4[2]) = [
    # ERC165의 ERC165 인터페이스 ID
    0x01ffc9a7,
    # ERC721의 ERC165 인터페이스 ID
    0x80ac58cd,
]

ERC-165 (새 탭에서 열림)는 컨트랙트가 애플리케이션과 통신하는 방법, 즉 어떤 ERC를 준수하는지 공개하는 메커니즘을 지정합니다. SUPPORTED_INTERFACES는 이 컨트랙트가 준수하는 두 개의 4바이트 인터페이스 ID(ERC-165 자체 및 ERC-721)의 상수 목록입니다.

함수

이것들은 실제로 ERC-721을 구현하는 함수들입니다.

생성자

@deploy
def __init__():

Python과 마찬가지로 Vyper에서 생성자 함수는 __init__이라고 합니다. 이 함수는 @deploy 데코레이션으로 표시되며, 이는 컨트랙트가 배포될 때 한 번 실행됨을 의미합니다.

    """
    @dev 컨트랙트 생성자입니다.
    """

Python과 Vyper에서는 여러 줄 문자열("""로 시작하고 끝남)을 지정하고 이를 어떤 방식으로도 사용하지 않음으로써 주석을 생성할 수도 있습니다. 이러한 주석에는 NatSpec (새 탭에서 열림)도 포함될 수 있습니다.

    self.minter = msg.sender

상태 변수에 액세스하려면 self.<variable name>을 사용합니다(이 역시 Python과 동일합니다). 생성자는 컨트랙트를 배포한 계정을 minter로 기록합니다.

뷰 함수

이들은 블록체인의 상태를 수정하지 않는 함수이므로, 외부에서 호출될 경우 무료로 실행될 수 있습니다. 뷰 함수가 컨트랙트에 의해 호출되는 경우 여전히 모든 노드에서 실행되어야 하므로 가스 비용이 발생합니다.

@view
@external

함수 정의 앞에 있는 골뱅이 기호(@)로 시작하는 이러한 키워드를 _데코레이션(decoration)_이라고 합니다. 이들은 함수가 호출될 수 있는 상황을 지정합니다.

  • @view는 이 함수가 뷰(view)임을 지정합니다.
  • @external은 이 특정 함수가 트랜잭션 및 다른 컨트랙트에 의해 호출될 수 있음을 지정합니다.
def supportsInterface(interface_id: bytes4) -> bool:

Python과 대조적으로 Vyper는 정적 타입 언어 (새 탭에서 열림)입니다. 데이터 타입 (새 탭에서 열림)을 식별하지 않고는 변수나 함수 매개변수를 선언할 수 없습니다. 이 경우 입력 매개변수는 4바이트 값인 bytes4이고, 출력은 부울 값입니다.

    """
    @dev 인터페이스 식별은 ERC-165에 지정되어 있습니다.
    @param interface_id 인터페이스의 ID
    """
    return interface_id in SUPPORTED_INTERFACES

interface_idSUPPORTED_INTERFACES 목록의 인터페이스 ID 중 하나인 경우 True를 반환합니다.

### 뷰 함수 ###

이들은 사용자와 다른 컨트랙트가 토큰에 대한 정보를 사용할 수 있게 해주는 뷰 함수입니다.

이 줄은 _ownerempty(address)로 작성된 제로 주소가 아님을 단언(assert) (새 탭에서 열림)합니다. 제로 주소인 경우 오류가 발생하고 작업이 되돌려집니다.

이더리움 가상 머신(EVM)에서 값이 저장되지 않은 모든 스토리지는 0입니다. _tokenId에 토큰이 없으면 self.idToOwner[_tokenId]의 값은 0입니다. 이 경우 함수는 되돌려집니다.

getApproved는 0을 반환할 있다는 점에 유의하세요. 토큰이 유효한 경우 self.idToApprovals[_tokenId]를 반환합니다. 승인자가 없으면 해당 값은 0입니다.

이 함수는 _operator가 이 컨트랙트에서 _owner의 모든 토큰을 관리하도록 허용되었는지 확인합니다. 여러 운영자가 있을 수 있으므로 이는 2단계 HashMap입니다.

전송 도우미 함수

이 함수들은 토큰 전송 또는 관리의 일부인 작업을 구현합니다.


### 전송 함수 도우미 ###

@view
@internal

이 데코레이션인 @internal은 동일한 컨트랙트 내의 다른 함수에서만 해당 함수에 액세스할 수 있음을 의미합니다. 관례적으로 이러한 함수 이름도 밑줄(_)로 시작합니다.

주소가 토큰을 전송하도록 허용되는 방법에는 세 가지가 있습니다:

  1. 주소가 토큰의 소유자인 경우
  2. 주소가 해당 토큰을 지출하도록 승인된 경우
  3. 주소가 토큰 소유자의 운영자인 경우

위의 함수는 상태를 변경하지 않으므로 뷰가 될 수 있습니다. 운영 비용을 줄이기 위해 뷰가 될 수 있는 모든 함수는 뷰가 되어야 합니다.

전송에 문제가 있는 경우 호출을 되돌립니다.

필요한 경우에만 값을 변경하세요. 상태 변수는 스토리지에 존재합니다. 스토리지에 쓰는 것은 EVM(이더리움 가상 머신)이 수행하는 가장 비용이 많이 드는 작업 중 하나입니다(가스 측면에서). 따라서 이를 최소화하는 것이 좋으며, 기존 값을 쓰는 것조차도 높은 비용이 듭니다.

토큰을 전송하는 방법에는 두 가지(일반 및 안전)가 있지만, 감사를 더 쉽게 하기 위해 코드에서 전송을 수행하는 위치를 단 한 곳으로 유지하고자 이 내부 함수를 사용합니다.

Vyper에서 이벤트를 발생시키려면 log 문을 사용합니다(자세한 내용은 여기를 참조하세요 (새 탭에서 열림)). 이벤트는 가져온 인터페이스에 속하므로 IERC721.Transfer로 참조하고 키워드를 통해 해당 필드를 전달합니다.

전송 함수

이 함수를 사용하면 임의의 주소로 전송할 수 있습니다. 해당 주소가 사용자이거나 토큰 전송 방법을 아는 컨트랙트가 아닌 한, 전송한 모든 토큰은 해당 주소에 갇혀 쓸모없게 됩니다.

IERC721 인터페이스가 transferFrom, safeTransferFrom, approve를 payable로 선언하므로, 인터페이스를 구현하는 컨트랙트는 해당 서명과 일치해야 하기 때문에 여기에 @payable 데코레이션이 있습니다.

문제가 발생하면 어차피 되돌려질 것이고 호출에서 수행된 모든 작업이 취소되므로 전송을 먼저 수행해도 괜찮습니다.

    if _to.is_contract: # `_to`가 컨트랙트 주소인지 확인합니다

먼저 주소가 컨트랙트인지(코드가 있는지) 확인합니다. 그렇지 않은 경우 사용자 주소로 간주하며 사용자는 토큰을 사용하거나 전송할 수 있습니다. 하지만 이것이 잘못된 보안 의식을 심어주게 해서는 안 됩니다. 아무도 개인 키를 모르는 주소로 토큰을 전송하면 safeTransferFrom을 사용하더라도 토큰을 잃을 수 있습니다.

        returnValue: bytes4 = extcall ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)

대상 컨트랙트를 호출하여 ERC-721 토큰을 받을 수 있는지 확인합니다. Vyper 0.4에서는 다른 컨트랙트에 대한 호출을 표시해야 하므로 호출 앞에 extcall이 붙습니다.

        # 전송 목적지가 'onERC721Received'를 구현하지 않는 컨트랙트인 경우 예외를 발생시킵니다
        assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes4)

목적지가 컨트랙트이지만 ERC-721 토큰을 수락하지 않는 경우(또는 이 특정 전송을 수락하지 않기로 결정한 경우) 되돌립니다.

관례적으로 승인자를 두지 않으려면 자신을 지정하는 것이 아니라 제로 주소를 지정합니다.

    # 요구 사항을 확인합니다
    senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
    senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
    assert (senderIsOwner or senderIsApprovedForAll)

승인을 설정하려면 소유자이거나 소유자가 권한을 부여한 운영자여야 합니다.

새 토큰 발행 및 기존 토큰 소각

컨트랙트를 생성한 계정은 새로운 NFT를 발행할 권한이 있는 슈퍼 유저인 minter입니다. 그러나 이 계정조차도 기존 토큰을 소각할 수는 없습니다. 소유자 또는 소유자가 승인한 엔터티만 소각할 수 있습니다.

### 발행 및 소각 함수 ###

@external
def mint(_to: address, _tokenId: uint256) -> bool:

이 함수는 항상 True를 반환합니다. 작업이 실패하면 되돌려지기 때문입니다.

발행자(ERC-721 컨트랙트를 생성한 계정)만 새 토큰을 발행할 수 있습니다. 이는 향후 발행자의 신원을 변경하려는 경우 문제가 될 수 있습니다. 프로덕션 컨트랙트에서는 발행자가 다른 사람에게 발행 권한을 전송할 수 있는 함수가 필요할 것입니다.

    # `_to`가 제로 주소인 경우 예외를 발생시킵니다
    assert _to != ZERO_ADDRESS
    # NFT를 추가합니다. `_tokenId`를 누군가 소유하고 있는 경우 예외를 발생시킵니다
    self._addTokenTo(_to, _tokenId)
    log Transfer(ZERO_ADDRESS, _to, _tokenId)
    return True

관례적으로 새 토큰의 발행은 제로 주소로부터의 전송으로 간주됩니다.

토큰을 전송하도록 허용된 사람은 누구나 토큰을 소각할 수 있습니다. 소각은 제로 주소로 전송하는 것과 같아 보이지만, 제로 주소는 실제로 토큰을 받지 않습니다. 이를 통해 토큰에 사용된 모든 스토리지를 확보할 수 있으며, 이는 트랜잭션의 가스 비용을 줄일 수 있습니다.

이 컨트랙트 사용하기

Solidity와 달리 Vyper에는 상속이 없습니다. 이는 코드를 더 명확하게 만들어 보안을 강화하기 위한 의도적인 설계 선택입니다. 따라서 자신만의 Vyper ERC-721 컨트랙트를 생성하려면 이 컨트랙트 (새 탭에서 열림)를 가져와 원하는 비즈니스 로직을 구현하도록 수정하면 됩니다.

결론

복습을 위해 이 컨트랙트에서 가장 중요한 몇 가지 아이디어를 정리해 보았습니다.

  • 안전한 전송으로 ERC-721 토큰을 받으려면 컨트랙트가 ERC721Receiver 인터페이스를 구현해야 합니다.
  • 안전한 전송을 사용하더라도 개인 키를 모르는 주소로 토큰을 보내면 토큰이 갇힐 수 있습니다.
  • 작업에 문제가 있을 때 단순히 실패 값을 반환하는 것보다 호출을 revert(되돌리기)하는 것이 좋습니다.
  • ERC-721 토큰은 소유자가 있을 때 존재합니다.
  • NFT 전송 권한을 부여받는 방법에는 세 가지가 있습니다. 소유자이거나, 특정 토큰에 대해 승인을 받거나, 소유자의 모든 토큰에 대한 운영자가 될 수 있습니다.
  • 과거 이벤트는 블록체인 외부에서만 볼 수 있습니다. 블록체인 내부에서 실행되는 코드는 이를 볼 수 없습니다.

이제 안전한 Vyper 컨트랙트를 구현해 보세요.

제 작업물은 여기에서 더 볼 수 있습니다 (새 탭에서 열림).