Como implementar NFTs con Solidity
Cibrax
Posted on October 28, 2021
NFT o Token No Fugible (Non-Fungile-Token) es un concepto relativamente nuevo y capaz dificil de entender para muchos.
Un token en el contexto de Blockchain representa una prueba de pertenencia en el mundo digital. Significa que se puede utilizar como evidencia para demostrar que uno es dueño de algo. Hay una diferencia muy sutil entre ser dueño y probar que uno es dueño. Vos podes ser dueño de un automovil, pero para probar que realmente te pertenece, quizas necesites una factura o titulo que lo demuestre. Un token sirve para demostrar esto ultimo. Ademas, podes demostrar que el token te pertenece utilizando el sistema de critografia con clave publica y privada. El token esta asociado a una direccion que solo se puede calcular con la clave privada que uno posee.
Los tokens se crearon para representar valor en la moneda nativa de un Blockchain o su equivalente en moneda fiat como ser los dolares americanos. Si tenes un token, lo podes cambiar por otro sin hacer ninguna diferencia o perder valor.
En cambio, los NFTs fueron creados para representar cosas unicas cuyo valor no esta atado a una moneda. Como cada NFT es unico, tambien tiene un valor intrinsico diferente. No se puede cambiar simplemente por otro NFT como ocurriria con un token tradicional. Se pueden utilizar para representar posesion sobre bienes digitales como ser imagenes, subscripciones, tickets o recompensas en un juego, etc.
NFTs en Ethrereum
Los NFTs se implementan en Ethereum a traves de Smart Contracts. Estos ultimos no son los NFT en si, pero proveen el mecanismo para generarlos. Los contratos en este escenario controlan como se generan y asignan los NFTs en el Blockchain. La imagen siguiente muestra como los NFTs se agrupan y viven bajo el contrato que los genero.
En ese sentido, uno no puede simplemente acceder o localizarlos directamente por una direccion en el Blockchain. Son solamente una representation logica dentro del ambito de un contrato. El acceso a los mismos esta exclusivamente controlada por dicho contrato.
Como no representan un elemento nativo de un Blockchain, un desarrollador podria implementar el contrato para manejarlos en la forma que mas le convenga. Para evitar que esto pase, la comunidad de Ethereum definio una interface con metodos que cualquier implementador deberia seguir para considerar que el contrato permite controlar NFTs. Esta interface se convirtio en un estandard con el nombre de ERC-721.
pragma solidity ^0.4.20;
/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
/// Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
/// @dev This emits when ownership of any NFT changes by any mechanism.
/// This event emits when NFTs are created (`from` == 0) and destroyed
/// (`to` == 0). Exception: during contract creation, any number of NFTs
/// may be created and assigned without emitting Transfer. At the time of
/// any transfer, the approved address for that NFT (if any) is reset to none.
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
/// @dev This emits when the approved address for an NFT is changed or
/// reaffirmed. The zero address indicates there is no approved address.
/// When a Transfer event emits, this also indicates that the approved
/// address for that NFT (if any) is reset to none.
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
/// @dev This emits when an operator is enabled or disabled for an owner.
/// The operator can manage all NFTs of the owner.
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
/// @notice Count all NFTs assigned to an owner
/// @dev NFTs assigned to the zero address are considered invalid, and this
/// function throws for queries about the zero address.
/// @param _owner An address for whom to query the balance
/// @return The number of NFTs owned by `_owner`, possibly zero
function balanceOf(address _owner) external view returns (uint256);
/// @notice Find the owner of an NFT
/// @dev NFTs assigned to zero address are considered invalid, and queries
/// about them do throw.
/// @param _tokenId The identifier for an NFT
/// @return The address of the owner of the NFT
function ownerOf(uint256 _tokenId) external view returns (address);
/// @notice Transfers the ownership of an NFT from one address to another address
/// @dev Throws unless `msg.sender` is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `_from` is
/// not the current owner. Throws if `_to` is the zero address. Throws if
/// `_tokenId` is not a valid NFT. When transfer is complete, this function
/// checks if `_to` is a smart contract (code size > 0). If so, it calls
/// `onERC721Received` on `_to` and throws if the return value is not
/// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
/// @param data Additional data with no specified format, sent in call to `_to`
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
/// @notice Transfers the ownership of an NFT from one address to another address
/// @dev This works identically to the other function with an extra data parameter,
/// except this function just sets data to "".
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
/// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
/// THEY MAY BE PERMANENTLY LOST
/// @dev Throws unless `msg.sender` is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `_from` is
/// not the current owner. Throws if `_to` is the zero address. Throws if
/// `_tokenId` is not a valid NFT.
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
/// @notice Change or reaffirm the approved address for an NFT
/// @dev The zero address indicates there is no approved address.
/// Throws unless `msg.sender` is the current NFT owner, or an authorized
/// operator of the current owner.
/// @param _approved The new approved NFT controller
/// @param _tokenId The NFT to approve
function approve(address _approved, uint256 _tokenId) external payable;
/// @notice Enable or disable approval for a third party ("operator") to manage
/// all of `msg.sender`'s assets
/// @dev Emits the ApprovalForAll event. The contract MUST allow
/// multiple operators per owner.
/// @param _operator Address to add to the set of authorized operators
/// @param _approved True if the operator is approved, false to revoke approval
function setApprovalForAll(address _operator, bool _approved) external;
/// @notice Get the approved address for a single NFT
/// @dev Throws if `_tokenId` is not a valid NFT.
/// @param _tokenId The NFT to find the approved address for
/// @return The approved address for this NFT, or the zero address if there is none
function getApproved(uint256 _tokenId) external view returns (address);
/// @notice Query if an address is an authorized operator for another address
/// @param _owner The address that owns the NFTs
/// @param _operator The address that acts on behalf of the owner
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
El concepto de interface tampoco existe en la maquina virtual de Ethereum (EVM). Solidity los soporta, pero una vez que el contrato se compila y transforma en byte code, no hay forma de determinar que implementa dicha interface salvo mirando los metodos que provee.
Como los contratos son la fuente de NFTs, tambien cobra sentido el echo de utilizarlos para agrupar colecciones como images or arte.
Un NFT no es otra cosa mas que un ID unico que esta asociado a la direccion de su dueño en el espacio de almacenamiento del Smart Contract que lo genero.
El espacio de almacenamiento en el Blockchain es caro. Mientras guardes datos basicos de tus NFTs, no pasa nada. Pero si quieres guardar cosas mas grandes como imagenes de varios KBs, ya estamos hablando de otra cosa. El Blockchain no esta optimizado para ese escenario y tambien el precio a pagar seria muy alto. Por esa razon, lo que se hace en estos casos es utilizar un archivo de metadatos. Es un archivo en formato JSON que describe a tu NFT mas en detalle, y guarda entre otras cosas, URLs a otras ubicaciones en donde estan las imagenes asociadas al NFT u otros documentos grandes. Este archivo de metadatos se podria guardar en cualquier lado, pero la mayoria prefiere hacerlo en forma decentralizada utilizando IPFS. El contrato termina manteniendo una referencia a la ubicacion de este archivo.
El problema del huevo y la gallina. El Blockchain asegura integridad de datos para mi NFTs. Una vez que se almacena ahi, nadie puede cambiarlo. Sin embargo, no podemos asegurar lo mismo para este archivo de metadatos que vive afuera del Blockchain. Hay varias soluciones a este problema. Una es guardar estos metadatos como texto en el contrato, y todas las referencias del mismo fuera del Blockchain. Otra es guardar un checksum generado a partir de los datos en el mismo.
Implementando un contrato ERC-721 con OpenZeppelin
OpenZeppelin ya provee un contrato que podemos utilizar de base para cualquier implementacion que se adhiera al estandard ERC-721.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract TikenToken is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
event TokenEmitted(string indexed symbol, address _sender, uint256 _id);
constructor() ERC721("My NFT", "MYNFT") {
}
function createToken(string memory _tokenURI, address to) public onlyOwner returns (uint) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(to, newItemId);
emit TokenEmitted(symbol(), to, newItemId);
return newItemId;
}
}
El codigo de arriba muestra una implementacion muy basica. Genera un nuevo identificador para un token NFT y llama al metodo interno _mint
para asociar este nuevo token con la direccion que se paso como argumento. Este contrato guarda internamente la relacion entre ambos en una estructura de datos del tipo mapping
.
Esta implementacion no ofrece metadatos para los NFTs. Si queres guardar una URL que apunte al archivo de metadatos, tambien tenes que implementar la interface ERC721URIStorage
.
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function createToken(string memory _tokenURI) public onlyOwner returns (uint) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(msg.sender, newItemId);
_setTokenURI(newItemId, _tokenURI);
emit TokenEmitted(symbol(), msg.sender, newItemId);
return newItemId;
}
Con estos cambios, hay un metodo nuevo tokenURI
que devuelve la URL asociada al token y modificamos el metodo createToken
para guardar esa URL en el contrato.
Por ultimo, si tambien quieran soportar destruir o quemar los token NFTs generados por su contrato, lo pueden hacer implementando la interface ERC721Burnable
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
Este metodo lo que hace es asignar el token a una direccion a la cual nadie puede acceder, y por lo tanto se considera que el token fue destruido o quemado.
Posted on October 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.