Token ERC-721 o token non fungibiliA differenza dei token ERC-20, i quali non hanno differenze l’uno dall’altro, i token non fungibili hanno principalmente lo scopo di rappresentare il possesso di un un'entità con caratteristiche uniche.
Partendo da github ho trovato questa implementazione:
https://github.com/m0t0k1ch1/ERC721-token-sample/tree/master/contracts che ho provveduto a modificare e semplificare per rendere più semplice la spiegazione e il funzionamento.
Avendo già illustrato in esempi precedenti come funzionano i token ERC-20 potrete notare come questi due standard condividono parte delle funzionalità.
Questa è l’interfaccia da implementare per usare lo standard ERC-721:
contract ERC721 {
function totalSupply() public view returns (uint256 total);
function balanceOf(address _owner) public view returns (uint256 balance);
function ownerOf(uint256 _tokenId) external view returns (address owner);
function approve(address _to, uint256 _tokenId) external;
function transfer(address _to, uint256 _tokenId) external;
function transferFrom(address _from, address _to, uint256 _tokenId) external;
event Transfer(address from, address to, uint256 tokenId);
event Approval(address owner, address approved, uint256 tokenId);
}
come potete vedere a prima vista è molto simile alla
ERC-20 e, controllando in dettaglio noterete che tutte le operazioni che riguardano i token non “parlano” di quantità ma di un Id (_tokenId).
Definizione generale sulle informazioni che il token detiene e informazioni su chi li possiede:
string public constant name = "Tokenized Item";
string public constant symbol = "NFTI";
struct Token { // Definizione delle informazioni mantenute dal token
address mintedBy;
uint64 mintedAt;
string description; /*Aggiunta allo scopo di rendere più comprensibile l’esempio, in realtà questo potrebbe essere un hash di un link IPFS o maggiori informazioni sul token senza incidere troppo sui costi derivanti dall’utilizzo delle informazioni all’interno dello smart contract*/
}
Token[] tokens; // Array dei token in esistenza
//Mappature
mapping (uint256 => address) public tokenIndexToOwner; //Token -> User
mapping (address => uint256) ownershipTokenCount; //User -> Token
mapping (uint256 => address) public tokenIndexToApproved; //Token -> User approvato a spendere il token
//Evento
event Mint(address owner, uint256 tokenId, string description);
Funzioni interne sono per lo più simili a quelle di un ERC-20, gestiscono internamente le operazioni per tenere traccia di:
- chi possiede un token
- a chi appartiene un token
- chi è autorizzato a spendere o muovere un token
tramite i mapping sopra citati.
function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
return tokenIndexToOwner[_tokenId] == _claimant;
}
function _approvedFor(address _claimant, uint256 _tokenId) internal view returns (bool) {
return tokenIndexToApproved[_tokenId] == _claimant;
}
function _approve(address _to, uint256 _tokenId) internal {
tokenIndexToApproved[_tokenId] = _to;
Approval(tokenIndexToOwner[_tokenId], tokenIndexToApproved[_tokenId], _tokenId);
}
function _transfer(address _from, address _to, uint256 _tokenId) internal {
ownershipTokenCount[_to]++;
tokenIndexToOwner[_tokenId] = _to;
if (_from != address(0)) {
ownershipTokenCount[_from]--;
delete tokenIndexToApproved[_tokenId];
}
Transfer(_from, _to, _tokenId);
}
è presente una funzione _mint per creare un nuovo token e assegnarnlo all’indirizzo specificato, che in questo caso, osservando la funzione pubblica mint coincide con chi esegue il metodo mint.
function _mint(address _owner, string description) internal returns (uint256 tokenId) {
Token memory token = Token({
mintedBy: _owner,
mintedAt: uint64(now),
description: description
});
tokenId = tokens.push(token) - 1;
Mint(_owner, tokenId, description);
_transfer(0, _owner, tokenId);
}
Queste sono le funzioni principali per testare la creazione e visualizzare delle informazioni del token tramite Remix (
https://remix.ethereum.org):
function mint(string description) external returns (uint256) {
return _mint(msg.sender,description);
}
function getToken(uint256 _tokenId) external view returns (address mintedBy, uint64 mintedAt,string description) {
Token memory token = tokens[_tokenId];
mintedBy = token.mintedBy;
mintedAt = token.mintedAt;
description = token.description;
}
Codice completo per test su Remix:Se avete domande o volete chiarimenti sono disponibile.
pragma solidity ^0.4.18;
contract ERC721 {
function totalSupply() public view returns (uint256 total);
function balanceOf(address _owner) public view returns (uint256 balance);
function ownerOf(uint256 _tokenId) external view returns (address owner);
function approve(address _to, uint256 _tokenId) external;
function transfer(address _to, uint256 _tokenId) external;
function transferFrom(address _from, address _to, uint256 _tokenId) external;
event Transfer(address from, address to, uint256 tokenId);
event Approval(address owner, address approved, uint256 tokenId);
}
contract NFTItem is ERC721 {
string public constant name = "Tokenized Item";
string public constant symbol = "NFTI";
/*** DATA TYPES ***/
struct Token {
address mintedBy;
uint64 mintedAt;
string description;
}
Token[] tokens;
mapping (uint256 => address) public tokenIndexToOwner;
mapping (address => uint256) ownershipTokenCount;
mapping (uint256 => address) public tokenIndexToApproved;
event Mint(address owner, uint256 tokenId, string description);
/*** INTERNAL FUNCTIONS ***/
function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
return tokenIndexToOwner[_tokenId] == _claimant;
}
function _approvedFor(address _claimant, uint256 _tokenId) internal view returns (bool) {
return tokenIndexToApproved[_tokenId] == _claimant;
}
function _approve(address _to, uint256 _tokenId) internal {
tokenIndexToApproved[_tokenId] = _to;
Approval(tokenIndexToOwner[_tokenId], tokenIndexToApproved[_tokenId], _tokenId);
}
function _transfer(address _from, address _to, uint256 _tokenId) internal {
ownershipTokenCount[_to]++;
tokenIndexToOwner[_tokenId] = _to;
if (_from != address(0)) {
ownershipTokenCount[_from]--;
delete tokenIndexToApproved[_tokenId];
}
Transfer(_from, _to, _tokenId);
}
function _mint(address _owner, string description) internal returns (uint256 tokenId) {
Token memory token = Token({
mintedBy: _owner,
mintedAt: uint64(now),
description: description
});
tokenId = tokens.push(token) - 1;
Mint(_owner, tokenId, description);
_transfer(0, _owner, tokenId);
}
/*** ERC721 IMPLEMENTATION ***/
function totalSupply() public view returns (uint256) {
return tokens.length;
}
function balanceOf(address _owner) public view returns (uint256) {
return ownershipTokenCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address owner) {
owner = tokenIndexToOwner[_tokenId];
require(owner != address(0));
}
function approve(address _to, uint256 _tokenId) external {
require(_owns(msg.sender, _tokenId));
_approve(_to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) external {
require(_to != address(0));
require(_to != address(this));
require(_owns(msg.sender, _tokenId));
_transfer(msg.sender, _to, _tokenId);
}
function transferFrom(address _from, address _to, uint256 _tokenId) external {
require(_to != address(0));
require(_to != address(this));
require(_approvedFor(msg.sender, _tokenId));
require(_owns(_from, _tokenId));
_transfer(_from, _to, _tokenId);
}
function tokensOfOwner(address _owner) external view returns (uint256[]) {
uint256 balance = balanceOf(_owner);
if (balance == 0) {
return new uint256[](0);
} else {
uint256[] memory result = new uint256[](balance);
uint256 maxTokenId = totalSupply();
uint256 idx = 0;
uint256 tokenId;
for (tokenId = 1; tokenId <= maxTokenId; tokenId++) {
if (tokenIndexToOwner[tokenId] == _owner) {
result[idx] = tokenId;
idx++;
}
}
}
return result;
}
function mint(string description) external returns (uint256) {
return _mint(msg.sender,description);
}
function getToken(uint256 _tokenId) external view returns (address mintedBy, uint64 mintedAt,string description) {
Token memory token = tokens[_tokenId];
mintedBy = token.mintedBy;
mintedAt = token.mintedAt;
description = token.description;
}
}