NFT(정적 NFT)는 "Non-Fungible Token"의 약자이며, 블록체인 상의 고유한 디지털 자산으로 대체가 불가능한 불변한 토큰
NFT는 일반적으로 ERC721 / ERC1155 토큰 표준을 기반으로 구축
NFT는 고유한 데이터가 담긴 토큰이다. 블록체인에서 NFT가 발행되면 이 NFT와 관련된 정보는 수정이 불가능한 것으로 알려져 있다. NFT 메타데이터는 토큰의 이름을 지정하고, 특성을 할당하고, 파일 링크를 배치하는 곳이다. 토큰ID가 소유권을 확인할 수 있는 영구 식별자를 제공하는 반면, 메타데이터는 NFT의 본질이며, 토큰을 유용하게 만드는 요소를 담고 있다.
현재 정적NFT는 대부분 NFT 아트 프로젝트 및 play-to-earn 게임 프로젝트와 디지털 수집품으로 사용된다.
대체 불가능한 토큰은 블록체인에 존재하는 디지털 객체이다. 모든 NFT는 1:1 토큰ID와 고유한 컨트랙트 주소를 통해서 서로 구별이 가능한데, 여기에 이미지, 동영상 파일 또는 기타 데이터와 같은 메타데이터를 첨부할 수 있으며, 이는 고유한 디지털 객체를 나타내는 토큰을 소유할 수 있다는 의미이다.
세계 최고의 NFT 거래소인 오픈씨는 ERC721 / ERC1155 표준 규격에 따르는 메타데이터의 구조에 대한 가이드라인을 제공한다.
이 모델은 정적NFT의 지속성에 의해 제한된다. 왜냐하면 블록체인이 발행된 후, 첨부된 메타데이터는 고정되기 때문이다.
정적NFT에서 메타데이터의 개념은 NFT 상품에 대해서 고유성을 부여하는 하나의 요소인데, 이것이 처음과는 다르게 변경이 되어진다면, 처음 NFT에 부여된 가치에 대한 보장이 이루어질지는 미지수인 것이다.
하지만, 현실에서는 변화하하는 오프라인 데이터들이 온라인에서 활용되기 위해서는 NFT들도 동적으로 토큰의 메타데이터를 변경할 필요가 있다.
예를 들어, NFT가 게임 캐릭터의 성장을 위한 플랫폼에서 운영된다면 캐릭터의 NFT 메타데이터는 성장과 함께 메타데이터 값이 변경되양 한다. 또한, 이외 추가로 필요한 정보를 넣어야 할 때 정적인 NFT는 변경을 하지 못해 활용을 할 수 없지만, 사용자가 Dynamic NFT를 소유한 고유 식별자임을 유지하면 외부 조건에 따라 메타데이터 값을 변경하고 업데이트를 하는 가능성을 제공할 수 있게된다.
Dynamic NFT(dNFT)는 외부 조건에 따라 자동으로 메타데이터를 변경할 수 있도록 인코딩된 스마트 계약 논리를 갖춘 대체 불가능한 토큰(NFT)이다.
Dynamic NFT는 고유 식별자를 유지하면서 메타데이터의 일부분을 업데이트할 수 있는 두가지 장점을 모두 갖춘 접근방식을 제공한다.
다시 말해, Dynamic NFT는 외부 조건에 따라 변경될 수 있는 NFT이고, 이 NFT의 변경은 스마트 컨트랙트를 통한 블록체인상의 메타데이터 변경을 의미한다.
제너레이티브 NFT 아트 프로젝트에는 종종 다양한 특성이 있으며, 일부 특성은 다른 특성보다 더 희귀하다. 이러한 특성은 NFT의 메타데이터에 해당 특성과 일치하는 이미지 또는 비디오에 대한 IPFS 링크와 함께 배치된다. Dynamic NFT에서는 이러한 특성이 외부 조건에 따라 변경된다.
Dynamic NFT의 메타데이터 변경은 외부 조건에 따라 여러 가지 방법을 트리거될 수 있다.
이러한 조건은 on-chain 및 off-chain 모두에서 존재할 수 있다. 대표적으로 체인링크는 off-chain의 데이터를 컨트랙트에 반영할 수 있도록 데이터 전달을 위한 데이터 피드 api 서비스를 제공한다.
메타데이터 변경 이외에도, 다이나믹한 요소들이 존재할 수 있다. 예를 들어, 게임 속에서 어떤 숨겨진 지점을 찾았을 때 mint되는 Dynamic NFT가 있을 수 있고, 또 메타데이터 내에 숨겨진 특성이 아닌 사용자 상호작용을 통해 나타날 수 있는 "숨겨진 특성"을 가질 수 도 있다.
실제로 Opensea에서도 Dynamic NFT를 볼 수 있다.
이 NFT의 컨트랙트 주소로 트랜잭션을 검색해보면
0x5bc18431304B4218e2b2BfFaA97E608f8E500544
불과 30분전에 Update Token Uri라는 트랜잭션이 발생한 것을 볼 수 있다.
컨트랙트 코드를 조금 더 깊이 살펴보자
contract NftInjectableTokenIdRolesUpdatableBaseURI is ERC721URIStorage, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
address private _owner;
bool public isFreezeTokenUris;
mapping (uint256 => bool) public freezeTokenUris;
string private baseURI;
event PermanentURI(string _value, uint256 indexed _id); // https://docs.opensea.io/docs/metadata-standards
event PermanentURIGlobal();
constructor(string memory _name, string memory _symbol, address owner, bool _isFreezeTokenUris, string memory _initBaseURI) ERC721(_name, _symbol) {
_setupRole(DEFAULT_ADMIN_ROLE, owner);
_setupRole(MINTER_ROLE, owner);
_setupRole(MINTER_ROLE, msg.sender);
isFreezeTokenUris = _isFreezeTokenUris;
baseURI = _initBaseURI;
_owner = owner;
}
function mintToCaller(address caller, uint256 tokenId, string memory tokenURI)
public onlyRole(MINTER_ROLE)
returns (uint256)
{
_safeMint(caller, tokenId);
_setTokenURI(tokenId, tokenURI);
return tokenId;
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, AccessControl)
returns (bool)
{
return ERC721.supportsInterface(interfaceId);
}
function owner() public view returns (address) {
return _owner;
}
function _baseURI()
internal
view
virtual
override(ERC721)
returns (string memory) {
return baseURI;
}
function updateTokenUri(uint256 _tokenId, string memory _tokenUri, bool _isFreezeTokenUri)
public
onlyRole(MINTER_ROLE) {
require(_exists(_tokenId), "NFT: update URI query for nonexistent token");
require(isFreezeTokenUris == false, "NFT: Token uris are frozen globally");
require(freezeTokenUris[_tokenId] != true, "NFT: Token is frozen");
require(_isFreezeTokenUri || (bytes(_tokenUri).length != 0), "NFT: Either _tokenUri or _isFreezeTokenUri=true required");
if (bytes(_tokenUri).length != 0) {
require(keccak256(bytes(tokenURI(_tokenId))) != keccak256(bytes(string(abi.encodePacked(_baseURI(), _tokenUri)))), "NFT: New token URI is same as updated");
_setTokenURI(_tokenId, _tokenUri);
}
if (_isFreezeTokenUri) {
freezeTokenUris[_tokenId] = true;
emit PermanentURI(tokenURI(_tokenId), _tokenId);
}
}
function update(string memory _newBaseURI, bool _freezeAllTokenUris)
public
onlyRole(MINTER_ROLE) {
require(isFreezeTokenUris == false, "NFT: Token uris are already frozen");
baseURI = _newBaseURI;
if (_freezeAllTokenUris) {
freezeAllTokenUris();
}
}
function freezeAllTokenUris()
public
onlyRole(MINTER_ROLE) {
require(isFreezeTokenUris == false, "NFT: Token uris are already frozen");
isFreezeTokenUris = true;
emit PermanentURIGlobal();
}
}
아직 정확하게 분석하고 파악한것은 아니지만, 얼핏 살펴봤을 때, polygonscan 트랜잭션이 호출한 메소드가 핵심인 것 같다.
function updateTokenUri(uint256 _tokenId, string memory _tokenUri, bool _isFreezeTokenUri)
public
onlyRole(MINTER_ROLE) {
require(_exists(_tokenId), "NFT: update URI query for nonexistent token");
require(isFreezeTokenUris == false, "NFT: Token uris are frozen globally");
require(freezeTokenUris[_tokenId] != true, "NFT: Token is frozen");
require(_isFreezeTokenUri || (bytes(_tokenUri).length != 0), "NFT: Either _tokenUri or _isFreezeTokenUri=true required");
if (bytes(_tokenUri).length != 0) {
require(keccak256(bytes(tokenURI(_tokenId))) != keccak256(bytes(string(abi.encodePacked(_baseURI(), _tokenUri)))), "NFT: New token URI is same as updated");
_setTokenURI(_tokenId, _tokenUri);
}
if (_isFreezeTokenUri) {
freezeTokenUris[_tokenId] = true;
emit PermanentURI(tokenURI(_tokenId), _tokenId);
}
}
이 부분에서 보면, 인터널 함수인 _setTokenURI를 통해 URI를 업데이트 해주고 있다.
결국 Dynamic NFT라는것이, 외부 / 내부의 트리거를 통해 메타데이터에 접근하는 tokenURI의 업데이트가 가능한 형태의 NFT라는 것을 확인할 수 있다.
이 컬렉션의 경우 시간을 기반으로한 외부 트리거에 의존하여 token URI를 계속 업데이트 하는 것이라 볼 수 있다.
이 컬렉션에 대해 자세히 살펴보면, AWS Lambd function을 이용하여 자동적으로 또, 주기적으로 트리거를 발생시키는 것을 알 수 있다.
아마존에서 제공하는 Basic Lambda function을 만들고 이를 이용해서 트리거를 만든다.
이것은 시간을 기반으로 NFT의 메타데이터를 업데이트 하는데, 경우에 따라서는 시간이 아닌, 날씨 API를 기반으로 날씨에 따라서 NFT의 메타데이터를 업데이트하도록 설정할 수 도있다.
이 컬렉션은
결국, 이처럼 기존의 NFT와는 다르게, 초기 설정한 메타데이터에 접근하는 tokenURI를 외부 / 내부의 트리거를 통해 변경시킴으로써 메타데이터를 다르게 매칭 시켜 줄 수 있는 기능을 가진것이 Dynamic NFT 인 것이다. 다음에 기회가 되면 외부트리거에 의존하는 것이 아닌 (특히 체인링크 서비스를 사용하는것이 아닌!) 내부트리거를 통해 메타데이터를 변경시키는 컨트랙트를 분석할 수 있었으면 좋겠다.
< 참고 >