NFT에 대해 정리한 글

NFT에 대해 정리한 글

2024년 4월 21일

정보 #

표준 #

스마트 컨트랙트가 준수해야되는 규칙이며 프로토콜과 비슷하다. 해당 표준을 준수하는 인터페이스가 있고, 인터페이스의 메서드(ERC-721 기준 transferFrom, approve, ownerOf)들을 구현해둬야 표준을 준수했다고 할 수 있게 된다.

기본적인것만 구현하면, 직접 사용할 것들은 추가로 구현해도 괜찮다. 표준을 준수해야 다른 앱들과 연동했을때 문제 없이 동작되게 할 수 있다.


ERC-721 #

하나의 컨트랙트에 하나의 콜렉션, 여러개의 토큰ID 마다 각각의 고유한 NFT를 의미하며 진정한 의미의 NFT(NonFungibleToken)
하나의 컨트랙트에서 하나의 콜렉션만 만들 수 있기 때문에 여러개의 콜렉션을 관리하려면 팩토리 컨트랙트에서 NFT 컨트랙트를 생성하는 방식(팩토리패턴)으로 구현해야 한번의 배포로 여러개의 콜렉션을 관리할 수 있게 된다.
OpenZeppelin라이브러리를 사용하면 아래의 표준 함수나 이벤트들이 이미 구현된 상태로 시작할 수 있고 필요한 부분만 오버라이드 해서 사용하면 된다.

1contract MyCustomNFT is ERC721 {
2    constructor(string memory name, string memory symbol) ERC721(name, symbol) {}
3
4    // OpenZeppelin의 _mint 함수를 오버라이드
5    function _mint(address to, uint256 tokenId) internal override {
6        // 커스텀 로직 추가
7        super._mint(to, tokenId); // 부모 컨트랙트의 _mint 함수 호출
8    }
9}

표준 함수 #

  • balanceOf(address _owner): _owner가 소유한 NFT의 수를 반환합니다. 이 함수는 0이 아닌 주소를 입력으로 받아야 합니다.
  • ownerOf(uint256 _tokenId): 토큰 ID _tokenId의 소유자 주소를 반환합니다.
  • safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data): _from 주소에서 _to 주소로 토큰 ID _tokenId를 안전하게 전송합니다. data는 추가 정보를 전달하는 데 사용됩니다.
  • safeTransferFrom(address _from, address _to, uint256 _tokenId): 위의 함수와 유사하지만, data 파라미터 없이 토큰을 전송합니다.
  • transferFrom(address _from, address _to, uint256 _tokenId): _from에서 _to로 토큰 ID _tokenId를 전송합니다. 이 함수는 토큰의 소유권이나 전송 권한을 검증해야 합니다.
  • approve(address _approved, uint256 _tokenId): 다른 주소 _approved에게 단일 토큰 _tokenId의 전송 권한을 부여합니다.
  • setApprovalForAll(address _operator, bool _approved): _operator가 호출자의 모든 NFT를 대신 전송할 수 있는 권한을 부여하거나 철회합니다.
  • getApproved(uint256 _tokenId): 토큰 ID _tokenId에 대해 현재 승인된 주소를 반환합니다.
  • isApprovedForAll(address _owner, address _operator): _operator가 _owner의 모든 토큰을 전송할 수 있는 권한을 가지고 있는지 여부를 반환합니다.
  • tokenURI(uint256 tokenId): 토큰 ID로 URI를 구하는 함수인데, 필수는 아니고 선택이지만, 대부분 구현한다.

표준 이벤트 #

  • Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId): 토큰이 전송될 때마다 발생합니다.
  • Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId): 토큰의 전송 권한이 부여될 때 발생합니다.
  • ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved): 소유자가 자신의 모든 NFT에 대한 전송 권한을 _operator에게 부여하거나 철회할 때 발생합니다.

팩토리 컨트랙트 구현 #

팩토리에서 생산할 NFT 컨트랙트

 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.8.0;
 3
 4import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
 5import "@openzeppelin/contracts/access/Ownable.sol";
 6
 7contract NFTCollection is ERC721URIStorage, Ownable {
 8    constructor(string memory name, string memory symbol) ERC721(name, symbol) {}
 9
10    function mintNFT(address recipient, uint256 tokenId) public onlyOwner {
11        _mint(recipient, tokenId);
12    }
13}

팩토리 컨트랙트
이렇게 구현하면 ERC-721로 동적으로 컬렉션을 만들고 관리할 수 있다는게 장점이지만, 동적으로 컬렉션을 만들때마다 NFT 컨트랙트가 블록체인 네트워크에 배포되기 때문에 배포에 대한 상당한 가스비용이 발생한다.

 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.8.0;
 3
 4import "./NFTCollection.sol";
 5
 6contract NFTFactory {
 7    // 생성된 NFT 컬렉션의 주소를 추적하는 배열
 8    address[] public collections;
 9
10    event CollectionCreated(address collectionAddress, string name, string symbol);
11
12    // NFT 컬렉션을 생성하는 함수
13    function createCollection(string memory name, string memory symbol) public {
14        NFTCollection newCollection = new NFTCollection(name, symbol);
15        collections.push(address(newCollection));
16
17        // 콜렉션이 생성됐다는걸 모든 노드들에게 알린다. 
18        // 수신자가 없을거라면 굳이 만들지 않아도된다. 
19        emit CollectionCreated(address(newCollection), name, symbol);
20
21        // 콜렉션을 생성한 주체에게 생성한 NFT 민팅
22        newCollection.mintNFT(msg.sender, symbol);
23    }
24
25    // 생성된 모든 컬렉션의 주소를 반환하는 함수
26    function getCollections() public view returns (address[] memory) {
27        return collections;
28    }
29}

ERC-1155 #

하나의 컨트랙트에서 여러개의 콜렉션을 만들 수 있고, 하나의 토큰 id도 여러개를 발행할 수 있으며 배치전송이 가능한 표준. ERC-721의 단점을 보완하기 위해 나온 방식.


배치 전송 #

한꺼번에 여러 토큰을 전송하는 방식인데, ERC-721은 기본적으로 지원하지 않는다.
커스텀해서 구현하고, NFT를 생성한 지갑에 전송할때만 사용할 것이다.
이더리움에서는 한번의 트랜잭션에서 여러번의 mint를 한다고 해도 한번의 트랜잭션으로 처리되기 때문에 batchMint 함수에서 for문 돌면서 mint해서 구현할 수 있다.

1contract NFTFactory {
2    function batchMint(string[] memory tokenURIs, uint256[4][] memory attributesList) public {
3        for (uint256 i = 0; i < tokenURIs.length; i++) {
4            uint256 tokenId = i;
5            nftContract.mintNFT(msg.sender, tokenId, tokenURIs[i], attributesList[i]);
6        }
7    }
8}

규칙 #

  • 언더바로 시작하는 함수는 내부 함수의 네이밍 규칙이다. (필수 규칙이 아니라 그냥 개발자들 사이의 약속)

Traits #

abe7266d-8162-4065-a69f-5a3723a75724

OpenSea에서 NFT를 눌러보면 각 NFT를 표현하는 Traits라는게 있는걸 알 수 있다. 이것은 사실 NFT 표준은 아니고 OpenSea에 제공하는 NFT의 메타데이터일뿐이다.
메타데이터는 _tokenURI 링크를 통해 가져올 수 있어야 하는데 보통은 ipfs에 NFT 메타데이터를 저장하고 그 링크를 _tokenURI로 저장하게 된다.
메타데이터를 OpenSea가 인식하도록 구현하려면 opensea의 docs를 확인해서 메타데이터의 포맷을 맞춰줘야 한다.
opensea:metadata-standards
대충 아래와 같은 형식이다.

 1{
 2  "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.", 
 3  "external_url": "https://openseacreatures.io/3", 
 4  "image": "https://storage.googleapis.com/opensea-prod.appspot.com/puffs/3.png", 
 5  "name": "Dave Starbelly",
 6  "attributes": [
 7    {"trait_type": "Color", "value": "Red"},
 8    {"trait_type": "Shape", "value": "Circle"},
 9    {"trait_type": "Size", "value": "Large"}
10  ]
11}
comments powered by Disqus