대체 불가능 토큰 (Non-fungible token)
교환 / 복제 / 삭제가 불가능하며, 고유성을 갖는 블록체인 기반 토큰이다.
민팅을 통해 NFT를 발행한다.
대표 NFT 거래 플랫폼 : OpenSea
화폐를 주조한다는 단어 Mint로부터 유례되어, NFT를 발행하는 행위를 의미한다.
NFT를 발행하게 되면, 디지털 파일은 블록체인에 저장된 디지털 자산이 된다.
NFT 민팅 방법
NFT 플랫폼 이용 (OpenSea 등)
Smart Contract를 이용한 민팅
절차 : Smart Contract 작성 → Smart Contract 배포 → NFT로 만드려는 디지털 에셋을 IPFS에 업로드 → Smart Contract를 이용한 NFT 민팅
<aside> 💡 NFT 발행에는 수수료가 부과되는데, NFT 플랫폼을 이용할 경우 더 큰 수수료가 필요하다.
</aside>
블록체인 기반으로 체결하는 계약
서면으로 이루어지던 계약을 코드로 구현하고, 특정 조건이 충족되었을 때 해당 계약이 이행되도록 하는 프로그램
이더리움 블록체인에 처음으로 도입 되었다.
Solidity라는 프로그래밍 언어를 사용한다. (참고 : Solidity)
EIP(Ethereum Improvement Proposals)에는 이더리움에 사용되는 Smart Contract에 대한 표준이 정의되어 있다.
<aside> 💡 EIP에는 Smart Contract 표준 외에도 프로토콜 명세, 클라이언트 API 등에 대한 내용을 포함하고 있다.
</aside>
NFT를 민팅하기 위해서는 민팅을 위한 Smart Contract를 작성하고, 이를 이더리움 블록체인에 배포해야 한다.
Smart Contract는 Solidity를 이용하여 작성된다.
컴파일, 테스트 및 배포에는 truffle을 사용한다.
테스트 및 개발 환경에서는 이더리움 메인넷을 사용하지 않고, 테스트넷 또는 Ganache(가나슈)를 이용한 로컬 블록체인을 사용한다.
이더리움 기반 NFT는 ERC-721 표준을 준수해야 한다. (참고 : EIP-721: Non-Fungible Token Standard)
ERC-721 문서에는 NFT를 위한 Smart Contract의 인터페이스가 명시되어 있다.
@openzeppelin/contracts 라이브러리에는 표준을 준수하는 Smart Contract가 미리 작성되어 있으며, 이를 기반으로 빠르게 Smart Contract를 개발할 수 있다.
<aside> 💡 아래의 내용은 truffle, @openzeppelin/contracts 설치 및 프로젝트 초기 설정과 ganache 설치 완료를 가정하고 작성되었습니다.
</aside>
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol"; // NFT 토큰마다 고유 주소가 필요한데, Counters Smart Contract를 이용하면 순차적으로 발급할 수 있음, 라이브러리성 Smart Contract
contract MyNFT is ERC721Enumerable, ERC721URIStorage, Ownable {
// Library는 using이라는 키워드와 함께 쓰인다.
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// Smart Contract가 이더리움 블록체인에 배포되는 시점에 실행
constructor() ERC721("MyNFT", "NFT") {}
// Web3.js를 이용할 때, public으로 선언된 함수만 javascript에서 호출할 수 있다.
function publishItem(address creator, string memory uriOfToken)
public
returns (uint256)
{
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
// ERC20과 달리 ERC721은 토큰 내에 소유주에 대한 정보가 기록된다.
// ERC20은 토큰이 탈취될 경우 소유권을 확인할 수 있는 방법이 없다.
_mint(creator, newTokenId);
_setTokenURI(newTokenId, uriOfToken);
return newTokenId;
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(from, to, tokenId);
}
function _burn(uint256 tokenId)
internal
override(ERC721, ERC721URIStorage)
{
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
function safeMint(address to, uint256 tokenId) public onlyOwner {
_safeMint(to, tokenId);
}
}
해당 SmartContract 코드는 한시간 만에 끝내는 NFT 개발 입문(이더리움 솔리디티로 구현하는 NFT-ERC721)와 OpenZeppelin 공식 문서를 참고하였습니다.
ERC721Enumerable Contract를 상속받게 되면, ERC-721 문서의 enumeration extension
을 사용할 수 있게 된다.
이를 통해 tokenByIndex
함수를 이용하여, 쉽게 해당 Smart Contract로 발행된 NFT의 정보를 받아올 수 있다.
publishItem
함수를 이용하여 NFT 민팅을 할 수 있다.
constructor와 publishItem
를 제외한 함수들은 ERC721Enumerable, ERC721URIStorage 두 Smart Contract를 상속 받으며 발생한 중복 함수 정의를 해결하기 위해 선언하였다.
<aside> 💡 ERC721Enumerable, ERC721URIStorage은 공통적으로 ERC721 함수를 상속한다.
</aside>
Ganache를 이용하여 로컬 블록체인을 실행합니다.
truffle-config.js
파일 내 network 객체 내 development 설정 주석을 해제합니다.
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
}
<aside> 💡 포트 번호는 Ganache에 설정된 정보와 일치해야 합니다.
</aside>
<aside> 💡 network 객체 내 다른 값을 부여하여, 메인넷 또는 테스트넷에 연결 가능합니다.
</aside>
Test 파일을 생성합니다.
const MyNFT = artifacts.require("MyNFT");
contract("MyNFT", async function (accounts) {
// 가상 계좌 중 하나 선택
const [owner] = accounts;
beforeEach(async function () {
// Constructor 호출
this.token = await MyNFT.new();
});
it("publish item", async function () {
const tokenId = await this.token.publishItem(
owner,
"<http://dev.sample.com/a/b/abc.jpg>"
);
console.log(tokenId);
});
});
<aside> 💡 artifacts.require
는 js의 require와 유사하며, Parameter로 전달되는 값은 Contract의 이름입니다.
</aside>
테스트를 실행합니다.
truffle test
배포에는 Truffle의 Migration을 이용합니다. (참고: Truffle 문서)
Migrate 파일을 생성합니다.
const MyNFT = artifacts.require("MyNFT");
module.exports = function (deployer) {
deployer.deploy(MyNFT);
};
Migrate를 실행하여 Smart Contract를 배포합니다.
truffle migrate
<aside> 💡 —network 옵션을 이용하여 테스트 서버가 아닌 다른 서버를 선택할 수 있습니다.
</aside>
IPFS(아이피에프에스)는 "InterPlanetary File System"의 약자로서, 분산형 파일 시스템에 데이터를 저장하고 인터넷으로 공유하기 위한 프로토콜이다.
IPFS는 데이터의 내용을 변환한 해시값을 이용하여 전 세계 여러 컴퓨터에 분산 저장되어 있는 콘텐츠를 찾아서 데이터를 조각조각으로 잘게 나눠서 빠른 속도로 가져온 후 하나로 합쳐서 보여주는 방식으로 작동한다.
Pinata 또는 Infura와 같은 IPFS 서비스를 이용한다.
IPFS에 등록해야 하는 파일 목록
NFT로 만들 디지털 에셋
NFT Metadata (참고 : OpenSea Metadata Standard)
해당 파일은 디지털 에셋 URI와 이름 등을 포함하고 있으며, 이 파일의 URI가 NFT에 등록된다.
NFT Metadata의 IPFS URI를 NFT 민팅에 사용한다.
Truffle Console에 접속합니다.
truffle console
배포된 Smart Contract 인스턴스를 변수에 담습니다.
instance = await DigitalPicture.deployed()
Smart Contract에 작성했던 publishItem
함수를 이용하여 NFT를 민팅합니다.
instance.publishItem({Wallet Address}, {NFT Meta IPFS URI})
<aside> 💡 이외에도 Web3.js 등을 이용하여 다양한 방법으로 민팅을 진행할 수 있다.
</aside>