O padrão ERC-721 foi proposto para NFTs (Non-Fungible Tokens). NFT refere-se aos tokens únicos, o que significa que cada token dentro do contrato inteligente será diferente dos outros. Pode ser usado para identificar coisas únicas, por exemplo, algum tipo de imagem, bilhetes de loteria, colecionáveis, pinturas, ou mesmo música, etc. Lembrando que o ERC-20 foi para os tokens fungíveis, caso você não tenha lido o artigo sobre ERC-20, considere lê-lo primeiro. O melhor projeto do mundo real para o exemplo ERC-721 é o Cryptokitties . É um jogo em cima do blockchain, confira este jogo incrível para conhecimento com diversão.
Mas como isso funciona? Como essas imagens, músicas e pinturas são armazenadas em contratos inteligentes?
Bem, essas coisas não são armazenadas dentro do contrato, em vez disso, o contrato apenas aponta para um ativo externo. Há um número, tokenId
mostrando a propriedade. O recurso principal é a imagem ou algum outro dado anexado ao tokenId por meio de um URI. Vamos mergulhar um pouco mais.
Atribuímos um URI, que é um texto JSON, ao ID do token. E esse texto JSON contém os detalhes e o caminho para a imagem ou qualquer outro ativo. E o usuário possui apenas o tokenid
. Por exemplo, meu endereço tem o saldo do tokenid
4, agora a imagem ou qualquer tipo de dado anexado a esse tokenid
será meu. Eu possuo esses dados porque possuo o tokenid
.
Agora temos que armazenar essas duas coisas, ou seja, a imagem e o texto JSON (URI) em algum lugar para que qualquer mercado NFT possa buscar a imagem e exibi-la em seu site. Você pode estar pensando por que precisamos do URI de metadados, por que não atribuir diretamente o URI da imagem ao ID do token? Bem, você deve se lembrar por que precisamos dos padrões ERC em primeiro lugar, sim, para que seja fácil para os aplicativos do lado do cliente se conectarem aos contratos. Da mesma forma, os metadados que escrevemos para NFTs devem estar em um formato adequado recomendado pelo mercado NFT para que eles possam buscar facilmente os dados desse arquivo JSON e exibir os dados em seu site.
Vamos ver a coisa de armazenamento agora.
Existem duas maneiras de armazenar a imagem e o URI. Podemos hospedá-lo on-chain (dentro do blockchain) ou off-chain (fora do blockchain).
Os dados on-chain são armazenados no blockchain, mas ocupam muito espaço que não é acessível. Pense em pagar milhares de reais na hora de implantar seu contrato. Não soa bem. Além disso, o blockchain também oferece armazenamento limitado, você não pode armazenar arquivos grandes nele.
Portanto, temos a opção de hospedar os dados fora do blockchain. Através do nosso site usando AWS ou qualquer outro serviço de nuvem. Mas isso mata a ideia principal de uma blockchain, ou seja, descentralização. E se o servidor travar ou alguém o hackear? Como haveria um único servidor hospedando todos os dados. Portanto, precisamos de algo mais robusto a ataques e também descentralizado, e esse algo é o IPFS (Inter Planetary File System), que é um sistema de armazenamento distribuído. O IPFS possui vários nós semelhantes à ideia de blockchain que armazena os dados. Mas você não precisa pagar nenhuma taxa. Podemos hospedar nossas imagens e também o arquivo JSON (URI) no IPFS e, ao fazer isso, forneceremos um CID exclusivo (sem sentido aleatório) que aponta diretamente para os dados e atribuímos um CID específico ao ID do token específico. Falaremos mais sobre a parte técnica depois primeiro vamos entender a interface para o padrão ERC-721.
Vamos ver como é a interface ERC-721.
Este padrão tem as seguintes funcionalidades:
Transferência de tokens de uma conta para outra.
Obtendo o saldo atual do token de uma conta.
Obtendo o proprietário de um token específico.
O fornecimento total do token disponível na rede.
Além dessas, também possui algumas outras funcionalidades, como aprovar que uma quantidade de token de uma conta possa ser movida por uma conta de terceiros.
Agora vamos dar uma olhada na interface do ERC-721.
pragma solidity ^0.4.20; interface ERC721 { event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function transferFrom(address _from, address _to, uint256 _tokenId) external payable; function approve(address _approved, uint256 _tokenId) external payable; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); function isApprovedForAll(address _owner, address _operator) external view returns (bool); } Break
function balanceOf(address _owner) external view returns (uint256);
2. ownerOf Localiza o proprietário de um NFT. Agora, esta função nos informa sobre o proprietário de um determinado NFT. Mantemos esses dados semelhantes aos anteriores. mapeamento(uint => endereço) saldos públicos; NFTs atribuídos a endereços zero são considerados inválidos e as consultas sobre eles devem gerar um erro.
function ownerOf(uint256 _tokenId) external view returns (address);
3. safeTransferFrom Transfere a propriedade de um NFT de um endereço para outro. Ele deve apenas definir o proprietário de um tokenId para o novo endereço e também atualizar o mapeamento balances
. Deve lançar um erro nos seguintes casos:
msg.sender
é o proprietário atual, um operador autorizado ou o endereço aprovado para este NFT_from
não é o proprietário atual__to_
é o endereço zero.__tokenId_
não é um NFT válido
Quando a transferência é concluída, esta função verifica se __to_
é um contrato inteligente (tamanho do código > 0). Nesse caso, ele chama a função onERC721Received
em __to_
e lança se o valor de retorno não for igual a este: bytes4(keccak256("onERC721Received(address, address,uint256, bytes)")).
O que foi isso? É por isso que a função é chamada de transferência segura, falaremos mais sobre isso mais tarde.
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
4. safeTransferFrom : Isso funciona de forma idêntica à função acima com um parâmetro de dados extra, exceto que esta função apenas define os dados como “ ”.
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
Como essas duas funções têm o mesmo nome?
Curiosidade: Ao contrário do Javascript, o Solidity oferece suporte à sobrecarga de função. significa que podemos definir duas funções com os mesmos nomes, a única condição é que os parâmetros sejam diferentes, fazendo diferença na assinatura da função. Não sabe o que são assinaturas de função? Aqui você vai. Link para responder
5. transferFrom transfere a propriedade de um NFT. Funciona da mesma forma que a função safeTransferFrom
, a única diferença é que não chama a chamada de segurança ( onERC721Received
) no destinatário. Assim, criando um risco de perder o NFT.
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
6. aprovar o mesmo conceito da função de aprovação ERC20, mas com mais funcionalidade e lógica. Para que esta função funcione, definimos um mapeamento aninhado: mapping(uint =>address) internal allowance;
Nesse mapeamento, o uint refere-se ao ID do token e ao endereço que é aprovado para executar operações nesse tokenId específico. Essa função de aprovação deve verificar se msg.sender
é o proprietário atual ou um operador do proprietário do token. Se essa verificação for aprovada, somente o mapeamento deverá ser atualizado. Qual é a operadora aqui? Veja a próxima função.
function approve(address _approved, uint256 _tokenId) external payable;
7. setApprovalForAll funciona como a função de aprovação anterior, mas em vez de aprovar um endereço para um único ID de token, essa função deve aprovar e endereçar para lidar com todos os tokens pertencentes a um determinado endereço. O que isso significa? Isso significa que você pode criar um operador de endereço para seus NFTs. Esse endereço será o proprietário de seus NFTs até que você revogue a propriedade. Desta vez, usamos novamente o mapeamento: mapping(address => mapping(address=>bool))internal operator;
O primeiro endereço define o booleano true ou false para o segundo endereço aprovar ou revogar, respectivamente.
function setApprovalForAll(address _operator, bool _approved) external;
8. getApproved é semelhante à função de permissão na interface ERC-20. Retorna o address
aprovado para um único NFT. Ele verifica o mapeamento que atualizamos na função de aprovação acima.
function getApproved(uint256 _tokenId) external view returns (address);
9. isApprovedForAll semelhante à função acima. Ele consulta se um address
é um operator
autorizado para outro address
. Em vez de retornar o address
aprovado de um determinado tokenId
, esta função deve retornar o address
do operator
para o address
fornecido que atualizamos na função setApprovalForAll
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
Transferência: emite quando a propriedade de qualquer NFT é alterada por qualquer mecanismo. Este evento é emitido quando os NFTs são criados (from == 0)
e destruídos (to == 0).
Exceção : Durante a criação do contrato, qualquer número de NFTs pode ser criado e atribuído sem emitir Transferência. No momento de qualquer transferência, o endereço aprovado para essa NFT (se houver) é redefinido como nenhum.
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
A aprovação é emitida quando o endereço aprovado para uma NFT é alterado ou reafirmado. O endereço zero indica que não há endereço aprovado. Quando um evento de transferência é emitido, isso também indica que o endereço aprovado para essa NFT (se houver) foi redefinido para nenhum.
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
ApproveForAll é emitido quando um operador é habilitado ou desabilitado para um proprietário. A operadora pode gerenciar todas as NFTs do proprietário.
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
Esta foi a interface para os contratos ERC 721. Agora vamos aprender mais sobre suas implementações e regras.
Antes da implementação, há mais uma coisa. Lembra que falamos sobre uma função chamada onERC721Received
quando falamos sobre a transferência de tokens? Vamos falar sobre isso agora. Se o destinatário de um token ERC-721 for um contrato, ele precisará ser funcional com os tokens ERC-721. Porque e se não houver funcionalidade para interagir com os tokens nesse contrato do receptor? Os tokens ficarão bloqueados nesse contrato para sempre. Ninguém pode levar esses tokens para qualquer endereço, pois não há funcionalidade. Para superar essa vulnerabilidade, o receptor do token ERC-721 deve implementar a interface do receptor, caso contrário, nenhum dos contratos pode enviar qualquer token ERC-721 para esse contrato. Vejamos a interface do receptor.
Esta interface contém apenas uma função para implementar e que é onERC721Received, esta função deve tratar o recebimento de uma NFT. O contrato inteligente ERC-721 chama essa função no destinatário após uma transfer
. Esta função pode lançar para reverter e rejeitar a transferência. Devoluções diferentes do valor mágico DEVEM resultar na reversão da transação.
interface ERC721TokenReceiver { function onERC721Received(address _operator,address _from,uint256 _tokenId,bytes _data) external returns(bytes4); }
Aqui você pode estar pensando como o contrato de token saberia se o receptor implementou a interface ERC721TokenReceiver
ou não? Portanto, antes de enviar os tokens, você deve primeiro verificar esta parte. Para isso, vamos ver a implementação openzeppelin do ERC-721. Aqui está a função privada que você deve chamar dentro do safeTransfer()
antes de enviar os tokens. Se retornar true, somente a transação deverá ocorrer.
Bem, isso não garante a funcionalidade de retirar as NFTs do contrato do receptor, pois está apenas verificando a implementação da função do receptor, mas não a funcionalidade de transferir as NFTs do contrato. Então, qual é o significado disso? Esse método pode nos dizer que o autor do contrato receptor estava pelo menos ciente desse método, o que significa que ele deve ter implementado a funcionalidade para transferir os NFTs para fora do contrato. E, claro, ninguém gostaria que seus tokens ficassem presos dentro de um contrato.
As interfaces acima eram obrigatórias para um contrato de token ERC-721. Agora, algumas interfaces são teoricamente opcionais, mas tornam o contrato mais claro e mais utilizável.
Estes são ERC721Metadata e ERC721Enumerable. Vamos pegá-los um por um.
interface ERC721Metadata/* is ERC721 */ { /// @notice A descriptive name for a collection of NFTs in this /// contract function name()external view returns (string _name); /// @notice An abbreviated name for NFTs in this contract function symbol()external view returns (string _symbol); /// @notice A distinct Uniform Resource Identifier (URI) for a given /// asset. /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined /// in RFC3986. The URI may point to a JSON file that conforms to /// the "ERC721Metadata JSON Schema". function tokenURI(uint256 _tokenId)external view returns (string);
ERC721Enumerable- Isso apenas fornece os dados sobre os NFTs.
interface ERC721Enumerable/* is ERC721 */ { /// @notice Count NFTs tracked by this contract /// @return A count of valid NFTs tracked by this contract, where /// each one of them has an assigned and queryable owner not equal /// to the zero address function totalSupply()external view returns (uint256); /// @notice Enumerate valid NFTs /// @dev Throws if `_index` >= `totalSupply()`. /// @param _index A counter less than `totalSupply()` /// @return The token identifier for the `_index`th NFT, /// (sort order not specified) function tokenByIndex(uint256 _index)external view returns (uint256); /// @notice Enumerate NFTs assigned to an owner /// @dev Throws if `_index` >= `balanceOf(_owner)` or if /// `_owner` is the zero address, representing invalid NFTs. /// @param _owner An address where we are interested in NFTs owned /// by them /// @param _index A counter less than `balanceOf(_owner)` /// @return The token identifier for the `_index`th NFT assigned to /// `_owner`, /// (sort order not specified) function tokenOfOwnerByIndex(address _owner,uint256 _index)external view returns(uint256);
Agora, como saberíamos se uma interface foi implementada por um contrato ou não? Para isso, precisamos sair do tópico, ou seja, ERC-165, que tem uma função:
function supportsInterface(bytes4 interfaceID) external view returns (bool);
Essa função usa o interfaceId que você deseja verificar como um argumento e o compara com o interfaceId com o qual deseja verificar. Vou lhe dizer o que é um interfaceId, tenha paciência comigo. Primeiro veja a implementação desta função pelo openzeppelin.
Vamos entender mais sobre interfaceId nesta função type(IERC721).interfaceId
;
interfaceID é obtido por duas operações principais.
1. keccak 256 haxixe
2. Operação XOR
keccak256 é um algoritmo que pega uma entrada e cospe alguma string aleatória em bytes. Agora vamos descobrir o keccak hash para esta função.
function supportsInterface(bytes4 interfaceID) external view returns (bool);
A sintaxe se parece com ṭthis.
bytes4(keccak256('supportsInterface(bytes4)')
;
Agora temos o keccak hash para esta função. Lembre-se, você não precisa passar toda a função dentro do parâmetro para keccak256, mas apenas a assinatura. A assinatura significa apenas o nome e o tipo de parâmetro, nem mesmo o nome do parâmetro está incluído. Depois de obter o hash keccak256 de todas as funções, realizamos a operação XOR entre elas e o resultado que obtemos é o interfaceId. Vamos ver como. As operações XOR pegam entradas e fornecem algumas saídas depois de compará-las. A saída true
se uma, e apenas uma, das entradas for verdadeira. Se ambas as entradas forem falsas ou ambas forem verdadeiras, a saída será false
.
Você não precisa entender a matemática por trás do XOR. Apenas lembre-se, você pega o hash keccak256
de cada função e os passa para o portão XOR e o valor obtido é o interfaceID. Portanto, a ideia principal é pegar o keccak hash das funções e obter sua saída XOR. essa saída é o interfaceId e depois disso, você pode verificar se um contrato tem o mesmo interfaceID. Se sim, isso significa que o contrato está implementando a interface desejada.
Não se preocupe, a solidity tornou isso fácil para você. Vamos aprender tomando como exemplo a interface para ERC721Metadata.
interface ERC721Metadata/* is ERC721 */ { function name()external view returns (string _name); function symbol()external view returns (string _symbol); function tokenURI(uint256 _tokenId)external view returns (string); }
Aqui temos três funções. Então eu escrevi este pedaço de código para fazer você entender. Observe aqui o símbolo de acento circunflexo ^
. Este símbolo representa a operação XOR entre dois valores.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.16 ; interface metadata { function name()external view returns (string memory _name ); function symbol()external view returns (string memory _symbol); function tokenURI(uint256 _tokenId)external view returns (string memory a); } contract { //old and lengthy way by applying the keccak256 algorithm and performing XOR. function getHashOldWay ()public pure returns(bytes4){ return bytes4(keccak256('name()')) ^ bytes4(keccak256('symbol()'))^ bytes4(keccak256('tokenURI(uint256)')); } //Improved way function getHashNewWay ()public pure returns(bytes4){ metadata a; return a.name.selector ^ a.symbol.selector ^ a.tokenURI.selector; } //this is the latest simplest way to get the interfaceId function getHashLatestWay ()public pure returns(bytes4){ return type(metadata).interfaceId; } }
Ohh nós fomos muito para o ERC165 que esquecemos do ERC721, vamos voltar a ele.
Este é o momento certo para entender os URIs de token que ajudam os vários mercados NFT a identificar os dados que você atribuiu a um determinado tokenId. Como Opensea é o mercado mais famoso, vamos tomá-lo como exemplo. A Opensea espera que seu contrato ERC721 retorne um tokenURI chamando uma função tokenURI()
. Vejamos o contrato openzeppelin para isso.
1. Mesmo URI base para todos os tokens, variando pelo tokenId.
Dessa forma, atribuímos um URI base para todos os tokens e depois concatenamos o tokenId a ele, obtendo o URI de metadados. Isso só é possível quando você atribuiu sua coleção de forma que o link seja o mesmo para todos os arquivos JSON de metadados, a única diferença seria o tokenId. Por exemplo,
https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/ Este link aponta para a pasta, o que significa que este é o baseURI.
Para obter um único metadado, precisamos acessar https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/1.json
E agora você precisa retornar o tokenURI do contrato de forma que ele vá diretamente para os metadados desse token específico e o mercado possa buscá-lo. Fazemos isso concatenando o baseURI com o tokenId e codificando-o com abi. Observe esta função abaixo.
function tokenURI(uint tokenId) override public view returns(string memory) { return (string(abi.encodePacked( "https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/",Strings.toString(tokenId),".json")) ); }
Esta é a função que o marketplace irá chamar para obter a URI e exibir todos os dados que fornecemos no arquivo JSON.
2. URIs diferentes para cada token.
Essa é outra maneira de atribuir URIs localizados em locais diferentes. E o link IPFS para um arquivo individual se parece com isso. https://ipfs.filebase.io/ipfs/Qma65D75em77UTgP5TYXT4ZK5Scwv9ZaNyJPdX9DNCXRWc
Desta forma, você não pode simplesmente concatenar o tokenId com baseURI para obter este link, em vez disso, este link deve ser anexado diretamente ao tokenId on-chain. E, para isso, observe o contrato ERC721URIStorage
acima, no qual estamos definindo um mapeamento privado para atribuir um URI a um tokenId específico. E posteriormente também definindo uma função que popula o mapeamento com uma determinada URI. E a função tokenURI
substitui a função pai adicionando uma nova funcionalidade de retornar o URI mapeado.
Mais uma coisa a observar é o formato do esquema JSON. O JSON deve conter os dados de forma padronizada para que o marketplace possa buscar os dados desejados e exibir os mesmos.
O esquema JSON padrão para ERC721.
{ "title": "Asset Metadata", "type": "object", "properties": { "name": { "type": "string", "description": "Identifies the asset to which this NFT represents" }, "description": { "type": "string", "description": "Describes the asset to which this NFT represents" }, "image": { "type": "string", "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive." } } }
Portanto, essas são as coisas que você deve saber se estiver interessado no desenvolvimento de contratos inteligentes ERC-721 (NFT).
Problemas :
Existem vários problemas com escalabilidade no padrão ERC721. O problema surge ao transferir os tokens porque o ERC-721 permite que você transfira um token por vez. Isso significa que, se você quiser enviar 5 NFTs para alguém, precisará realizar 5 transações. Isso é bastante claro para você quanto espaço seria necessário no blockchain. É por isso que nas corridas de touro a rede enfrenta problemas, pois há muita pressa na rede e as tarifas de gás aumentam rapidamente.
ERC-1155 resolve esses problemas. Como? Vamos falar sobre isso em um post separado. Se você entendeu o padrão ERC721 completamente, o ERC1155 não será muito difícil de entender.
Publicado também aqui .