---
title: "Vyper ERC-721 컨트랙트 연습"
description: "Ryuya Nakamura의 ERC-721 컨트랙트와 작동 방식"
author: "오리 포메란츠"
lang: ko
tags: ["Vyper", "erc-721", "Python"]
skill: beginner
breadcrumb: "Vyper ERC-721"
published: 2021-04-01
---

## 소개 {#introduction}

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

이 글에서는 [Ryuya Nakamura의 ERC-721 컨트랙트](https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC721.vy)를 분석해 보겠습니다.
이 컨트랙트는 Solidity보다 안전하지 않은 코드를 작성하기 어렵게 설계된 Python과 유사한 컨트랙트 언어인 [Vyper](https://vyper.readthedocs.io/en/latest/index.html)로 작성되었습니다.

## 컨트랙트 {#contract}

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

Vyper의 주석은 Python과 마찬가지로 해시(`ethereum.ercs`)로 시작하여 줄 끝까지 이어집니다. `@<keyword>`가 포함된 주석은 사람이 읽을 수 있는 문서를 생성하기 위해 [NatSpec](https://vyper.readthedocs.io/en/latest/natspec.html)에서 사용됩니다.

```python
from vyper.interfaces import ERC721

implements: ERC721
```

ERC-721 인터페이스는 Vyper 언어에 내장되어 있습니다.
[여기에서 코드 정의를 확인할 수 있습니다](https://github.com/vyperlang/vyper/blob/master/vyper/builtin_interfaces/ERC721.py).
인터페이스 정의는 Vyper가 아닌 Python으로 작성됩니다. 인터페이스는 블록체인 내부뿐만 아니라 외부 클라이언트(Python으로 작성될 수 있음)에서 블록체인으로 트랜잭션을 전송할 때도 사용되기 때문입니다.

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

```python
#pragma version >0.3.10
```

```python
#pragma version >0.3.10
```
### ERC721Receiver 인터페이스

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

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

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

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

```python
            _operator: address,
            _from: address,
```

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

```python
            _tokenId: uint256,
```

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

```python
            _data: Bytes[1024]
```

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

```python
        ) -> bytes4: nonpayable
```

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

[이벤트](/developers/docs/smart-contracts/anatomy/#events-and-logs)는 블록체인 외부의 사용자와 서버에 이벤트를 알리기 위해 발생(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` 객체로, [두 유형 사이에 존재하는 단방향 매핑](https://vyper.readthedocs.io/en/latest/types.html#mappings)입니다.

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

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

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

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

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

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

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

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

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

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

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

[ERC-165](https://eips.ethereum.org/EIPS/eip-165)는 컨트랙트가 애플리케이션과 통신하는 방법, 즉 어떤 ERC를 준수하는지 공개하는 메커니즘을 지정합니다. `SUPPORTED_INTERFACES`는 이 컨트랙트가 준수하는 두 개의 4바이트 인터페이스 ID(ERC-165 자체 및 ERC-721)의 상수 목록입니다.
### 함수 {#functions}

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

#### 생성자

```python
@deploy
def __init__():
```

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

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

Python과 Vyper에서는 여러 줄 문자열(`"""`로 시작하고 끝남)을 지정하고 이를 어떤 방식으로도 사용하지 않음으로써 주석을 생성할 수도 있습니다. 이러한 주석에는 [NatSpec](https://vyper.readthedocs.io/en/latest/natspec.html)도 포함될 수 있습니다.

```python
    self.minter = msg.sender
```

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

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

```python
@view
@external
```

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

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

```python
def supportsInterface(interface_id: bytes4) -> bool:
```

Python과 대조적으로 Vyper는 [정적 타입 언어](https://ko.wikipedia.org/wiki/%ED%83%80%EC%9E%85_%EC%8B%9C%EC%8A%A4%ED%85%9C#%EC%A0%95%EC%A0%81_%ED%83%80%EC%9E%85_%EA%B2%80%EC%82%AC)입니다. [데이터 타입](https://vyper.readthedocs.io/en/latest/types.html)을 식별하지 않고는 변수나 함수 매개변수를 선언할 수 없습니다. 이 경우 입력 매개변수는 4바이트 값인 `bytes4`이고, 출력은 부울 값입니다.

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

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

```python
### 뷰 함수 ###
```

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

```python
@view
@external
def balanceOf(_owner: address) -> uint256:
    """
    @dev `_owner`가 소유한 NFT의 수를 반환합니다.
         `_owner`가 제로 주소인 경우 예외를 발생시킵니다. 제로 주소에 할당된 NFT는 유효하지 않은 것으로 간주됩니다.
    @param _owner 잔액을 조회할 주소.
    """
    assert _owner != empty(address)
```

이 줄은 `_owner`가 `empty(address)`로 작성된 제로 주소가 아님을 [단언(assert)](https://vyper.readthedocs.io/en/latest/statements.html#assert)합니다. 제로 주소인 경우 오류가 발생하고 작업이 되돌려집니다.

```python
    return self.ownerToNFTokenCount[_owner]

@view
@external
def ownerOf(_tokenId: uint256) -> address:
    """
    @dev NFT 소유자의 주소를 반환합니다.
         `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다.
    @param _tokenId NFT의 식별자.
    """
    owner: address = self.idToOwner[_tokenId]
    # `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다
    assert owner != empty(address)
    return owner
```

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

```python
@view
@external
def getApproved(_tokenId: uint256) -> address:
    """
    @dev 단일 NFT에 대해 승인된 주소를 가져옵니다.
         `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다.
    @param _tokenId 승인을 조회할 NFT의 ID.
    """
    # `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다
    assert self.idToOwner[_tokenId] != empty(address)
    return self.idToApprovals[_tokenId]
```

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

```python
@view
@external
def isApprovedForAll(_owner: address, _operator: address) -> bool:
    """
    @dev `_operator`가 `_owner`에 대해 승인된 운영자인지 확인합니다.
    @param _owner NFT를 소유한 주소.
    @param _operator 소유자를 대신하여 행동하는 주소.
    """
    return (self.ownerToOperators[_owner])[_operator]
```

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

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

```python

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

@view
@internal
```

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

```python
def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool:
    """
    @dev 주어진 지출자(spender)가 주어진 토큰 ID를 전송할 수 있는지 여부를 반환합니다.
    @param spender 조회할 지출자의 주소
    @param tokenId 전송할 토큰의 uint256 ID
    @return bool msg.sender가 주어진 토큰 ID에 대해 승인되었는지,
        소유자의 운영자인지, 또는 토큰의 소유자인지 여부
    """
    owner: address = self.idToOwner[_tokenId]
    spenderIsOwner: bool = owner == _spender
    spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId]
    spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender]
    return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll
```

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

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

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

```python
@internal
def _addTokenTo(_to: address, _tokenId: uint256):
    """
    @dev 주어진 주소에 NFT를 추가합니다.
         `_tokenId`를 누군가 소유하고 있는 경우 예외를 발생시킵니다.
    """
    # `_tokenId`를 누군가 소유하고 있는 경우 예외를 발생시킵니다
    assert self.idToOwner[_tokenId] == empty(address)
    # 소유자를 변경합니다
    self.idToOwner[_tokenId] = _to
    # 개수 추적을 변경합니다
    self.ownerToNFTokenCount[_to] += 1


@internal
def _removeTokenFrom(_from: address, _tokenId: uint256):
    """
    @dev 주어진 주소에서 NFT를 제거합니다.
         `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다.
    """
    # `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다
    assert self.idToOwner[_tokenId] == _from
    # 소유자를 변경합니다
    self.idToOwner[_tokenId] = empty(address)
    # 개수 추적을 변경합니다
    self.ownerToNFTokenCount[_from] -= 1
```

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

```python
@internal
def _clearApproval(_owner: address, _tokenId: uint256):
    """
    @dev 주어진 주소의 승인을 지웁니다.
         `_owner`가 현재 소유자가 아닌 경우 예외를 발생시킵니다.
    """
    # `_owner`가 현재 소유자가 아닌 경우 예외를 발생시킵니다
    assert self.idToOwner[_tokenId] == _owner
    if self.idToApprovals[_tokenId] != empty(address):
        # 승인을 재설정합니다
        self.idToApprovals[_tokenId] = empty(address)
```

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

```python
@internal
def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address):
    """
    @dev NFT의 전송을 실행합니다.
         `msg.sender`가 현재 소유자, 권한이 있는 운영자, 또는 이 NFT에 대해 승인된
         주소가 아닌 경우 예외를 발생시킵니다. (참고: 비공개 함수에서는 `msg.sender`가 허용되지 않으므로 `_sender`를 전달합니다.)
         `_to`가 제로 주소인 경우 예외를 발생시킵니다.
         `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다.
         `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다.
    """
```

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

```python
    # 요구 사항을 확인합니다
    assert self._isApprovedOrOwner(_sender, _tokenId)
    # `_to`가 제로 주소인 경우 예외를 발생시킵니다
    assert _to != empty(address)
    # 승인을 지웁니다. `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다
    self._clearApproval(_from, _tokenId)
    # NFT를 제거합니다. `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다
    self._removeTokenFrom(_from, _tokenId)
    # NFT를 추가합니다
    self._addTokenTo(_to, _tokenId)
    # 전송을 기록합니다
    log IERC721.Transfer(sender=_from, receiver=_to, token_id=_tokenId)
```

Vyper에서 이벤트를 발생시키려면 `log` 문을 사용합니다([자세한 내용은 여기를 참조하세요](https://vyper.readthedocs.io/en/latest/event-logging.html#event-logging)). 이벤트는 가져온 인터페이스에 속하므로 `IERC721.Transfer`로 참조하고 키워드를 통해 해당 필드를 전달합니다.
#### 전송 함수

```python

### 전송 함수 ###

@external
@payable
def transferFrom(_from: address, _to: address, _tokenId: uint256):
    """
    @dev `msg.sender`가 현재 소유자, 권한이 있는 운영자, 또는 이 NFT에 대해 승인된
         주소가 아닌 경우 예외를 발생시킵니다.
         `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다.
         `_to`가 제로 주소인 경우 예외를 발생시킵니다.
         `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다.
    @notice 호출자는 `_to`가 NFT를 받을 수 있는지 확인할 책임이 있으며, 그렇지 않으면
            영구적으로 손실될 수 있습니다.
    @param _from NFT의 현재 소유자.
    @param _to 새로운 소유자.
    @param _tokenId 전송할 NFT.
    """
    self._transferFrom(_from, _to, _tokenId, msg.sender)
```

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

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

```python
@external
@payable
def safeTransferFrom(
        _from: address,
        _to: address,
        _tokenId: uint256,
        _data: Bytes[1024]=b""
    ):
    """
    @dev 한 주소에서 다른 주소로 NFT의 소유권을 전송합니다.
         `msg.sender`가 현재 소유자, 권한이 있는 운영자, 또는 이 NFT에 대해
         승인된 주소가 아닌 경우 예외를 발생시킵니다.
         `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다.
         `_to`가 제로 주소인 경우 예외를 발생시킵니다.
         `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다.
         `_to`가 스마트 컨트랙트인 경우 `_to`에서 `onERC721Received`를 호출하고
         반환 값이 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`가 아니면 예외를 발생시킵니다.
    @param _from NFT의 현재 소유자.
    @param _to 새로운 소유자.
    @param _tokenId 전송할 NFT.
    @param _data 지정된 형식이 없는 추가 데이터로, `_to` 호출 시 전송됩니다.
    """
    self._transferFrom(_from, _to, _tokenId, msg.sender)
```

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

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

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

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

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

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

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

```python
@external
@payable
def approve(_approved: address, _tokenId: uint256):
    """
    @dev NFT에 대해 승인된 주소를 설정하거나 재확인합니다. 제로 주소는 승인된 주소가 없음을 나타냅니다.
         `msg.sender`가 현재 NFT 소유자이거나 현재 소유자의 권한이 있는 운영자가 아닌 경우 예외를 발생시킵니다.
         `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다. (참고: 이는 EIP에 작성되어 있지 않습니다)
         `_approved`가 현재 소유자인 경우 예외를 발생시킵니다. (참고: 이는 EIP에 작성되어 있지 않습니다)
    @param _approved 주어진 NFT ID에 대해 승인될 주소.
    @param _tokenId 승인될 토큰의 ID.
    """
    owner: address = self.idToOwner[_tokenId]
    # `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다
    assert owner != empty(address)
    # `_approved`가 현재 소유자인 경우 예외를 발생시킵니다
    assert _approved != owner
```

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

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

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

```python
    # 승인을 설정합니다
    self.idToApprovals[_tokenId] = _approved
    log IERC721.Approval(owner=owner, approved=_approved, token_id=_tokenId)


@external
def setApprovalForAll(_operator: address, _approved: bool):
    """
    @dev 제3자("운영자")가 `msg.sender`의 모든 자산을 관리할 수 있도록 승인을 활성화하거나 비활성화합니다.
         또한 ApprovalForAll 이벤트를 발생시킵니다.
         `_operator`가 `msg.sender`인 경우 예외를 발생시킵니다. (참고: 이는 EIP에 작성되어 있지 않습니다)
    @notice 이는 발신자가 당시 토큰을 소유하고 있지 않더라도 작동합니다.
    @param _operator 권한이 있는 운영자 집합에 추가할 주소.
    @param _approved 운영자가 승인된 경우 True, 승인을 취소하려면 False.
    """
    # `_operator`가 `msg.sender`인 경우 예외를 발생시킵니다
    assert _operator != msg.sender
    self.ownerToOperators[msg.sender][_operator] = _approved
    log IERC721.ApprovalForAll(owner=msg.sender, operator=_operator, approved=_approved)
```
#### 새 토큰 발행 및 기존 토큰 소각 {#mint-burn}

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

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

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

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

```python
    """
    @dev 토큰을 발행하는 함수
         `msg.sender`가 발행자가 아닌 경우 예외를 발생시킵니다.
         `_to`가 제로 주소인 경우 예외를 발생시킵니다.
         `_tokenId`를 누군가 소유하고 있는 경우 예외를 발생시킵니다.
    @param _to 발행된 토큰을 수신할 주소.
    @param _tokenId 발행할 토큰 ID.
    @return 작업이 성공했는지 여부를 나타내는 부울.
    """
    # `msg.sender`가 발행자가 아닌 경우 예외를 발생시킵니다
    assert msg.sender == self.minter
```

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

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

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

```python

@external
def burn(_tokenId: uint256):
    """
    @dev 특정 ERC-721 토큰을 소각합니다.
         `msg.sender`가 현재 소유자, 권한이 있는 운영자, 또는 이 NFT에 대해 승인된 주소가 아닌 경우 예외를 발생시킵니다.
         `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다.
    @param _tokenId 소각할 ERC-721 토큰의 uint256 ID.
    """
    # 요구 사항 확인
    assert self._isApprovedOrOwner(msg.sender, _tokenId)
    owner: address = self.idToOwner[_tokenId]
    # `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다
    assert owner != ZERO_ADDRESS
    self._clearApproval(owner, _tokenId)
    self._removeTokenFrom(owner, _tokenId)
    log Transfer(owner, ZERO_ADDRESS, _tokenId)
```

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

## 이 컨트랙트 사용하기 {#using-contract}

Solidity와 달리 Vyper에는 상속이 없습니다. 이는 코드를 더 명확하게 만들어 보안을 강화하기 위한 의도적인 설계 선택입니다. 따라서 자신만의 Vyper ERC-721 컨트랙트를 생성하려면 [이 컨트랙트](https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC721.vy)를 가져와 원하는 비즈니스 로직을 구현하도록 수정하면 됩니다.

## 결론 {#conclusion}

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

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

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

[제 작업물은 여기에서 더 볼 수 있습니다](https://cryptodocguy.pro/).
