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 #
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}