Recentemente comecei um tutorial de programação de smart contract multi-token em Solidity, usando o padrão ERC-1155 aqui no blog e esta é a segunda parte deste tutorial. Caso não tenha implementado a primeira parte ainda, ela é obrigatória e pode ser encontrada neste link.
Na primeira parte implementamos um contrato MyTokens e todas as funções e eventos obrigatórios do padrão. No entanto o padrão é beeem minimalista, o que quer dizer que ele apenas determina como se dará a interação para transferência, delegação e verificação de saldo dos tokens registrados no contrato. Ele não determina por exemplo como você pode mintar/cunhar tokens, destrui-los, impedir o minting de novos e por aí vai, tarefas bem comuns nesse meio.
Além disso, você deve ter sentido falta do conteúdo dos tokens em si, uma imagenzinha pelo menos. Afinal, tudo o que registramos até o momento foram tokens representados por ids. Mas se eu sou o dono do token de id 1, o que afinal isso representa? Uma imagem? Um MP3? Um imóvel físico? O padrão define como os metadados podem ser armazenados e falaremos disso também nesta parte 2.
Vamos lá!
Minting
Duas tarefas muito comuns em contratos de tokens são o Minting e o Burning. Mas o que são eles? Vamos começar explicando minting e futuramente volto no Burning.
Minting é o ato de cunhar tokens e usamos esta palavra ao invés de criar ou gerar em alusão ao processo físico de cunhagem de moedas, muito usado na antiguidade. Mintar um token nada mais é do que criá-lo de fato, geralmente já transferindo a sua propriedade para alguém. E embora uma função de mint não esteja presente no padrão ERC-1155, ela é quase onipresente nos contratos deste tipo. Mas antes de partir para o minting, vamos definir algumas novas variáveis de estado em nosso contrato.
1 2 3 4 5 |
uint[] public _currentSupply = [50, 50, 50]; uint public _tokenPrice = 0.01 ether; |
Em um cenário 100% NFT, de tokens não fungíveis, eu poderia assumir que cada id possui apenas uma unidade disponível, certo? Em um cenário 100% fungível (ERC20) eu poderia usar apenas um id e definir (ou não) um total supply, certo? E em um cenário semi-fungível multi-token, como fazemos? Neste caso descrevemos o supply máximo de cada tipo de token em uma estrutura de dado, como fiz com o array _currentSupply. Usando o próprio id do token como índice, é possível consultar o supply disponível de cada tipo de token.
E por último, defini o preço base de um token do nosso contrato, ou seja, o quanto vamos cobrar de quem quiser adquirir uma unidade, tendo abaixo uma sugestão de como implementar essa função.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function mint(uint _tokenId) external payable { require(_tokenId < 3, "Invalid token id"); require(_currentSupply[_tokenId] > 0, "Max supply reached"); require(msg.value >= _tokenPrice, "Insufficient payment"); _balances[_tokenId][msg.sender] = 1; _currentSupply[_tokenId] -= 1; emit TransferSingle(msg.sender, address(0), msg.sender, _tokenId, 1); require( msg.sender.code.length == 0 || ERC1155TokenReceiver(msg.sender).onERC1155Received( msg.sender, address(0), _tokenId, 1, "" ) == ERC1155TokenReceiver.onERC1155Received.selector, "unsafe recipient" ); } |
Começamos com algumas validações básicas se o id do token é válido, se o supply máximo do token já não foi atingido e se o requisitante está pagando o valor exigido pelo mesmo. Tudo dando certo, nós assumimos que o msg.sender será o dono do token mintado, por isso adicionamos o token no mapping de _balances, decrementamos o supply disponível para aquele tipo de token e emitimos o evento de transferência, para sinalizar (aprenda a escutar eventos aqui).
Entenda esta função de mint com URI apenas como uma sugestão pois ela pode variar enormemente.
A função acima te dá uma ideia de como mintar, mas não ajuda muito quando o assunto é o quê mintar. Se eu quiser registrar uma foto? Um MP3? Como faço? Nesses casos, embora tecnicamente seja possível salvar os bytes do arquivo digital na blockchain, o mais comum é salvarmos apenas a URL do arquivo em questão, que estará armazenado em outro local (geralmente IPFS), para economizar nas transações, principalmente de grandes coleções. Essa abordagem é tão comum que é prevista na especificação 1155 como um opcional, definido pela interface abaixo (copie e cole no seu arquivo MyTokens.sol, junto das demais interfaces).
1 2 3 4 5 |
interface ERC1155Metadata_URI { function uri(uint256 _id) external view returns (string memory); } |
Essa interface define que os contratos multi-token devem possuir uma função que, dado um token id, retorne a URL para um JSON com os metadados do mesmo, cujo formato também é sugerido na especificação como sendo abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
{ "name": "Asset Name", "description": "Lorem ipsum...", "image": "https:\/\/s3.amazonaws.com\/your-bucket\/images\/{id}.png", "properties": { "simple_property": "example value", "rich_property": { "name": "Name", "value": "123", "display_value": "123 Example Value", "class": "emphasis", "css": { "color": "#ffffff", "font-weight": "bold", "text-decoration": "underline" } }, "array_property": { "name": "Name", "value": [1,2,3,4], "class": "emphasis" } } } |
Resumindo: o JSON do token deve ter as informações pertinentes ao mesmo, o que for necessário, e deve ser hospedado publicamente na Internet (sendo que o mais comum é utilizar a IPFS para isso). Se você já trabalhou com a ERC721 antes não deve ter visto nenhuma novidade aqui.
Implementando Metadados
Para servir de exemplo de implementação de metadados (o JSON que citei acima), vamos adicionar a função exigida pela extensão. Eu não vou mostrar aqui como criar o JSON ou como hospedá-lo, pois foge do escopo do tutorial. Vou partir do pressuposto que você já possui esses JSON em algum lugar ou que vai criá-los posteriormente.
Primeiro, adicione a interface ERC1155Metadata_URI no seu MyTokens.sol, junto das outras interfaces. Depois, ajuste o seu contrato MyTokens para adicionar a herança dessa interface também, bem como temos de ajustar a função supportsInterface para sinalizar que estamos suportando também a nova interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
contract MyTokens is ERC1155, ERC165, ERC1155Metadata_URI { function supportsInterface(bytes4 interfaceID) external pure returns (bool) { return interfaceID == 0x01ffc9a7 || // ERC-165 interfaceID == 0x4e2312e0 || // ERC-1155 interfaceID == 0x0e89341c;//ERC1155Metadata_URI } |
Depois, o próximo passo é implementarmos a função obrigatória da nova interface: uri.
1 2 3 4 5 6 |
function uri(uint256 _tokenId) external pure returns (string memory) { require(_tokenId < 3, "This token does not exists"); return string.concat("https://www.luiztools.com.br/", Strings.toString(_tokenId), ".json"); } |
Essa URL vai ser gerada automaticamente toda vez que a função for chamada, usando uma URL base, o id do token e a função JSON. Atenção que estou usando aqui a função string.concat, disponível a partir da versão 0.8.12 do Solidity, e a função Strings.toString, da biblioteca Strings.sol da OpenZeppelin, que você deve importar no seu contrato como abaixo (atenção que se estiver usando HardHat deve instalar a lib de contratos do OpenZeppelin e importá-la de acordo).
1 2 3 |
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol"; |
Essa biblioteca tem várias funções utilitárias muito úteis para manipulação de strings.
Opcionalmente você pode incluir no seu contrato funções para mudar a URL dos metadados, caso mude o local onde os arquivos JSON estejam armazenados. Mas se fizer isso, a especificação exige que você dispare um evento específico de mudança de URI, então esteja atento.
Burning
Outra função extremamente relevante para alguns projetos é a de burning. Burning é o ato de destruir (queimar, no inglês) um token por qualquer que seja o motivo. Na nossa estrutura de contrato pode implementar uma função de burn da seguinte maneira.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function burn( address _from, uint256 _tokenId ) public { require(_from != address(0), "ERC1155: burn from the zero address"); require(_tokenId < 3, "This token does not exists"); require(_balances[_tokenId][_from] >= 1, "Insufficient balance" ); require(_isApprovedOrOwner(_from, msg.sender), "You do not have permission"); _balances[_tokenId][_from] -= 1; emit TransferSingle(msg.sender, _from, address(0), _tokenId, 1); } |
Comecei fazendo uma série de verificações, sendo que só recebo o dono do token e o id do token a ser queimado:
- primeiro, o dono do token não pode ser zero;
- segundo, o id do token deve ser válido;
- terceiro, o dono do token deve ter pelo menos uma unidade do mesmo (vamos queimar uma de cada vez nesse exemplo);
- quarto, ele deve ser o requisitante do burn ou ter dado permissão ao requisitando (approveForAll);
Após essas verificações todas, eu reduzo o balance do dono do token e emito o evento que sou obrigado pelo padrão ERC1155, avisando que houve uma transferência ligada à carteira de endereço zero.
E com isso finalizamos mais esta etapa no desenvolvimento de nosso contrato multi-token. Conseguimos cobrir o principal nessas duas partes mas o céu é o limite quando o assunto é smart contract, então tendo qualquer dúvida, deixe nos comentários.
Agora é a hora de você aprender como os profissionais fazem contratos ERC-1155, o que vou abordar no próximo tutorial, que você confere neste link. E se quiser aprender a reduzir o consumo de gás nos seus smart contracts, este é o artigo certo.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.