Desconstruindo o Contrato Inteligente da Art Blocks
Art Blocks é uma plataforma para criar NFTs generativos on-chain. Mas você sabe o que é realmente mantido on-chain vs off-chain? E por que eles precisam do JavaScript em seu contrato inteligente?
Vamos descobrir desconstruindo o contrato inteligente da Art Blocks. Também aprenderemos como as imagens são geradas/renderizadas e onde a Art Blocks obtém a aleatoriedade necessária para gerá-las.
Aqui está o esboço deste artigo
- Histórico do ERC-721 — o padrão NFT
- Código-fonte do contrato da Art Blocks
- Gerando a arte
ERC-721 — o padrão NFT
Primeiro, um pouco do histórico sobre a Art Blocks.
Art Blocks é uma plataforma (na verdade apenas um contrato inteligente) onde você pode criar NFTs generativos. Artistas enviam scripts que podem gerar imagens. A Art Blocks armazena esses scripts e quando alguém deseja cunhar um NFT, ela cria um hash exclusivo. Este hash é usado como semente para o algoritmo de geração de imagem e a imagem gerada será exclusiva de quem cunhou.
Aqui estão alguns exemplos de imagens geradas:
Coleções populares da Art Blocks: Ringers, Chromie Squiggle, Fidenza.
Para entender o contrato inteligente da Art Blocks, primeiro precisamos aprender sobre ERC-721. ERC-721 é um padrão usado para implementar contratos inteligentes NFT. Para ser considerado compatível com ERC-721, um contrato precisa implementar estas funções:
pragma solidity ^0.4.20;
interface ERC721 {
function name() public view returns (string);
function symbol() public view returns (string);
function tokenURI(uint256 _tokenId) public view returns (string);
function totalSupply() public view returns (uint256);
function tokenByIndex(uint256 _index) public view returns (uint256);
function tokenOfOwnerByIndex(address _owner, uint256 _index) public view returns (uint256);
function balanceOf(address _owner) public view returns (uint256);
function ownerOf(uint256 _tokenId) public view returns (address);
function approve(address _approved, uint256 _tokenId) public payable;
function transferFrom(address _from, address _to, uint256 _tokenId) public payable;
}
-
name
esymbol
são descritores NFT. Por exemplo, para a Art Blocks, eles são “Blocos de Arte” e “BLOCOS”. -
tokenUri
- caminho para metadados do token (url de imagem, atributos de raridade, etc) -
totalSupply
- contar NFTs rastreados por este contrato -
tokenByIndex
- retorna tokenId do token no índice especificado. índice é [0, oferta total). -
tokenOfOwnerByIndex
- enumerar tokens do proprietário e retornar tokenId no índice -
balanceOf
- número de NFTs que o proprietário possui -
ownerOf
- proprietário do token especificado -
approve
- permitir que outra pessoa gerencie (transfira, venda, etc) seu token. Usado por terceiros, como OpenSea, para gerenciar tokens. (Existe uma função semelhantesetApprovalForAll (address _operator, bool _approved
) que é como aprovar, mas dá permissão para todos os tokens em vez de apenas um. Ignorado por brevidade) -
transferFrom
- transferir o token. O chamador precisa ser um endereço pré-aprovado.
Todos os contratos inteligentes NFT precisam implementar o padrão ERC-721. Isso permite que terceiros como a OpenSea interajam com os contratos NFT de forma padronizada (todos os contratos terão a mesma função ownerOf, por exemplo). Confira meu artigo sobre o detalhamento do contrato inteligente do BoredApeYachtClub para saber mais sobre o padrão ERC-721.
Vamos agora aprender como a Art Blocks implementa esse padrão e cria NFTs generativos.
Código-fonte do contrato da Art Blocks
O back-end blockchain da Art Blocks consiste em apenas um grande contrato inteligente chamado GenArt721Core.sol.
Este contrato inteligente é dividido em 2 partes:
- um contrato de implementação do padrão ERC-721
- o principal contrato
GenArt721Core.sol
responsável por armazenar os dados necessários para renderização de NFTs
GenArt721Core.sol
herda do contrato ERC-721. O código fonte pode ser encontrado no Etherscan e Github.
A Art Blocks também possui mais 2 contratos leves: GenArt721Minter
(cunha tokens e aceita pagamentos) e Randomizer
(gera números pseudo-aleatórios). Mas estes não serão abordados neste artigo.
Implementação do ERC-721
A Art Blocks implementa a interface ERC-721 usando uma implementação pronta do OpenZeppelin. OpenZeppelin é uma biblioteca de implementações dos padrões mais comuns.
A implementação não tem surpresas. Tudo o que você esperaria de uma implementação padrão:
- Eles usam mapeamentos para gerenciar a propriedade de tokens:
pragma solidity ^0.5.0;
// Mapeamento do ID do token para o proprietário
mapping (uint256 => address) private _tokenOwner;
// Mapeamento do proprietário para o número de tokens pertencentes
mapping (address => Counters.Counter) private _ownedTokensCount;
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "ERC721: consulta de saldo para o endereço zero");
return _ownedTokensCount[owner].current();
}
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _tokenOwner[tokenId];
require(owner != address(0), "ERC721: consulta do proprietário por token inexistente");
return owner;
}
- Veja como a propriedade é transferida:
pragma solidity ^0.5.0;
function transferFrom(address from, address to, uint256 tokenId) public {
// ...
_ownedTokensCount[from].decrement();
_ownedTokensCount[to].increment();
_tokenOwner[tokenId] = to;
// ...
}
- e como as aprovações são gerenciadas:
pragma solidity ^0.5.0;
// Mapeamento do ID do token para o endereço aprovado
mapping (uint256 => address) private _tokenApprovals;
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner, "ERC721: aprovação para o atual proprietário");
require(msg.sender == owner || isApprovedForAll(owner, msg.sender),
"ERC721: aprovar o chamador não é proprietário nem aprovado para todos"
);
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
- Embora não faça parte do padrão ERC-721, a implementação ERC-721 do OpenZeppelin inclui funções
mint
eburn
:
pragma solidity ^0.5.0;
function _mint(address to, uint256 tokenId) internal {
_tokenOwner[tokenId] = to;
_ownedTokensCount[to].increment();
}
function _burn(address owner, uint256 tokenId) internal {
_ownedTokensCount[owner].decrement();
_tokenOwner[tokenId] = address(0);
}
- A implementação tem mais alguns mapeamentos para armazenar informações adicionais (as funções setter/getter para esses mapeamentos serão omitidas por brevidade):
pragma solidity ^0.5.0;
// Mapeamento do proprietário para a lista de IDs de token de propriedade
mapping(address => uint256[]) private _ownedTokens;
// Mapeamento do ID do token para o índice da lista de tokens do proprietário
mapping(uint256 => uint256) private _ownedTokensIndex;
// Array com todos os IDs de token, usados para enumeração
uint256[] private _allTokens;
// Mapeamento do ID do token para a posição no array allTokens
mapping(uint256 => uint256) private _allTokensIndex;
- Finalmente, aqui estão o resto das funções do ERC-721:
pragma solidity ^0.5.0;
function totalSupply() public view returns (uint256) {
return _allTokens.length;
}
function tokenByIndex(uint256 index) public view returns (uint256) {
require(index < totalSupply(), "ERC721Enumerable: índice global fora dos limites");
return _allTokens[index];
}
function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256) {
require(index < balanceOf(owner), "ERC721Enumerable: índice proprietário fora dos limites");
return _ownedTokens[owner][index];
}
- A única função restante da especificação ERC-721,
tokenUri
, será explicada posteriormente neste artigo.
O contrato principal:GenArt721Core.sol
O contrato principal estende o contrato ERC-721 para adicionar funcionalidades específicas a Art Blocks: “armazenar informações do projeto” e “gerar NFTs”. Vamos começar com a parte de armazenamento de informações do projeto.
Armazenando informações do projeto
Cada coleção NFT é considerada um projeto separado (como Chromie Squiggle, Ringers, etc). O contrato principal define uma estrutura de dados para um projeto:
pragma solidity ^0.5.0;
struct Project {
string name;
string artist;
string description;
string website;
string license;
bool active;
bool locked;
bool paused;
// número de NFTs cunhados para este projeto
uint256 invocations;
uint256 maxInvocations;
// Scripts Javascript usados para gerar as imagens
uint scriptCount; // número de scripts
mapping(uint256 => string) scripts; // armazena cada script como uma string
string scriptJSON; // metadados de script, como de quais bibliotecas depende
bool useHashString; // se verdadeiro, o hash é usado como entrada para gerar a imagem
// seja projeto dinâmico ou estático
bool dynamic;
// se o projeto for dinâmico, tokenUri será "{projectBaseUri}/{tokenId}"
string projectBaseURI;
// se o projeto for estático, usará IPFS
bool useIpfs;
// tokenUri será "{projectBaseIpfsURI}/{ipfsHash}"
string projectBaseIpfsURI;
string ipfsHash;
}
Os NFTs de todos os projetos são armazenadas em um grande contrato inteligente — não criamos um novo contrato para cada coleção. Todos os projetos são armazenados em um grande mapeamento, chamado projects
, onde a chave é apenas o índice do projeto (0,1,2,…):
pragma solidity ^0.5.0;
mapping(uint256 => Project) projects;
uint256 public nextProjectId = 3;
function addProject(
string memory _projectName,
address _artistAddress,
uint256 _pricePerTokenInWei,
bool _dynamic) public onlyWhitelisted {
uint256 projectId = nextProjectId;
projectIdToArtistAddress[projectId] = _artistAddress;
projects[projectId].name = _projectName;
projectIdToCurrencySymbol[projectId] = "ETH";
projectIdToPricePerTokenInWei[projectId] = _pricePerTokenInWei;
projects[projectId].paused=true;
projects[projectId].dynamic=_dynamic;
projects[projectId].maxInvocations = ONE_MILLION;
if (!_dynamic) {
projects[projectId].useHashString = false;
} else {
projects[projectId].useHashString = true;
}
nextProjectId = nextProjectId.add(1);
}
Como você deve ter notado na captura de tela acima, o contrato usa mais algumas estruturas de dados para acompanhar tudo:
pragma solidity ^0.5.0;
//Todas as funções financeiras são retiradas da estrutura do projeto para visibilidade
mapping(uint256 => address) public projectIdToArtistAddress;
mapping(uint256 => string) public projectIdToCurrencySymbol;
mapping(uint256 => address) public projectIdToCurrencyAddress;
mapping(uint256 => uint256) public projectIdToPricePerTokenInWei;
mapping(uint256 => address) public projectIdToAdditionalPayee;
mapping(uint256 => uint256) public projectIdToAdditionalPayeePercentage;
mapping(uint256 => uint256) public projectIdToSecondaryMarketRoyaltyPercentage;
mapping(uint256 => string) public staticIpfsImageLink;
mapping(uint256 => uint256) public tokenIdToProjectId;
mapping(uint256 => uint256[]) internal projectIdToTokenIds;
mapping(uint256 => bytes32) public tokenIdToHash;
mapping(bytes32 => uint256) public hashToTokenId;
Deixe-me explicar as últimas 4 linhas:
-
tokenId
é a ID de um NFT eprojectId
é a ID do projeto. O contrato acompanha o mapeamento bidirecional entre os dois. -
hash
é o valor de hash keccak256 da combinação de [1) índice do NFT, 2) número do bloco, 3) hash do bloco anterior, 4) endereço que cunhou, 5) valor aleatório de um contrato randomizador]. Chegaremos ao contrato do randomizador daqui a pouco. O valorhash
é calculado durante a função mint:
Os parâmetros do projeto podem ser alterados pelos artistas através de vários fixadores como estes:
pragma solidity ^0.5.0;
function updateProjectName(
uint256 _projectId,
string memory _projectName)
onlyUnlocked(_projectId)
onlyArtistOrWhitelisted(_projectId) public {
projects[_projectId].name = _projectName;
}
function updateProjectDescription(
uint256 _projectId,
string memory _projectDescription)
onlyArtist(_projectId) public {
projects[_projectId].description = _projectDescription;
}
function toggleProjectIsLocked(uint256 _projectId)
public onlyWhitelisted onlyUnlocked(_projectId) {
projects[_projectId].locked = true;
}
Mas uma vez que o projeto está bloqueado, muitas variáveis não poderão mais ser alteradas.
Isso é tudo para a funcionalidade “armazenar informações do projeto”. Vamos passar para a próxima funcionalidade implementada pelo contrato GenArt721Core.sol
.
Gerando a arte
O ponto de entrada para gerar a arte é a função tokenUri
. É uma das funções do padrão ERC-721 e deve retornar os metadados (como imagens ou atributos) do NFT. Aqui está a implementação de tokenUri
:
pragma solidity ^0.5.0;
function tokenURI(uint256 _tokenId) external
view onlyValidTokenId(_tokenId) returns (string memory) {
// se staticIpfsImageLink estiver presente,
// então retorne "{projectBaseIpfsURI}/{staticIpfsImageLink}"
if (bytes(staticIpfsImageLink[_tokenId]).length > 0) {
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseIpfsURI,
staticIpfsImageLink[_tokenId]);
}
// se o projeto não for dinâmico e o useIpfs for verdadeiro,
// então retorne "{projectBaseIpfsURI}/{ipfsHash}"
if (!projects[tokenIdToProjectId[_tokenId]].dynamic
&& projects[tokenIdToProjectId[_tokenId]].useIpfs) {
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseIpfsURI,
projects[tokenIdToProjectId[_tokenId]].ipfsHash);
}
// senão retornar "{projectBaseURI}/{_tokenId}"
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseURI,
Strings.uint2str(_tokenId));
}
Ele tem muitas condições if, mas basicamente está apenas construindo o caminho de metadados condicionalmente. Os projetos têm a opção de armazenar os metadados em IPFS (como imagem ou arquivo JSON) ou, se o projeto for dinâmico, os metadados podem ser servidos a partir de uma API HTTP tradicional. A maioria dos projetos são dinâmicos, então vamos nos concentrar nesse caso.
Por exemplo, a coleção Fidenza (projectId=78
) tem o seguinte caminho de metadados:
Você pode obter essas informações do Etherscan. Basta rolar para baixo até "tokenURI". Se navegarmos para este caminho HTTP, obtemos este arquivo JSON:
Observe que o arquivo JSON tem várias informações diferentes para tipos de características e descrições de projetos. Ele também tem um link para a imagem real:
Então, o que você realmente possui quando compra um NFT? Nesse caso, você apenas possui o tokenId
. A função tokenUri
que mapeia o tokenId
para o link IPFS ou HTTP, dependendo das configurações do projeto. Este link aponta diretamente para a imagem ou para um JSON que possui atributos e um link aninhado para a imagem.
Mas como a imagem é gerada/renderizada? Infelizmente, a imagem não é gerada on-chain. O contrato inteligente armazena apenas um script JavaScript necessário para renderizar a imagem. O frontend da Art Blocks consulta esse script e gera a imagem sob demanda em seu backend tradicional, não no backend blockchain.
Por que a imagem não é gerada/renderizadaon-chain? É porque os scripts têm dependências de biblioteca. Os scripts dependem de bibliotecas JavaScript comuns, como p5.js e processing, que são comumente usadas por designers para criar imagens generativas. Seria muito caro colocar essas bibliotecas de dependência on-chain e é por isso que as imagens são geradas off-chain.
As instruções para renderizar imagens (os scripts de renderização) são armazenadas on-chain. Você pode verificar os scripts armazenados por si mesmo navegando pelo projectScriptInfo
no Etherscan. Isso mostrará qual dependência de biblioteca o script do projeto precisa e quantos scripts ele possui (se o script for muito longo, ele será dividido em várias partes):
Os scripts reais estão em projectScriptByIndex
:
Os scripts são armazenados como strings simples na estrutura de dados do Projeto:
Como a aleatoriedade é gerada?
Você pode se perguntar como os padrões aleatórios nas coleções NFT são gerados. Ao gerar as imagens, o frontend não extrai apenas os scripts do contrato inteligente. Ele também puxa a string de hash. Lembra-se da sequência de hash?
Esse hash pode ser lido do contrato do mapeamento tokenIdToHash
. A string de hash é usada como entrada/semente durante o processo de geração da imagem. A sequência de hash controla os parâmetros da imagem (por exemplo, quão ondulado o Chromie Squiggle se torna).
Muitas informações são combinadas para produzir o hash. Uma das entradas é o endereço de quem cunha. Desta forma, quem cunha participa do processo de geração da imagem e o NFT torna-se exclusivo para quem cunhar. (Se outra pessoa cunhasse o mesmo token nas mesmas condições exatas, obteria uma imagem diferente porque seu endereço seria diferente).
Outra entrada para o hash é o returnValue
de um randomizerContract
. Parece que este contrato não é de código aberto (não verificado no Etherscan), então não podemos ver seu código. Mas é mais provável que seja um gerador de números pseudo-aleatórios que gera números aleatórios on-chain de fontes como o número do bloco da última cunhagem.
Isso é tudo para o detalhamento do contrato da Art Blocks! Eu espero que isto tenha sido útil. Deixe-me saber nos comentários se você tiver alguma dúvida.
Estou planejando fazer mais desconstruções de contratos inteligentes populares, como Algorithmic da Stablecoin UST e NFT factory da thirdweb.
Você também pode conferir desconstruções de outros contratos inteligentes e mais coisas para noobs do Solidity em solidnoob.com.
Quer Conectar? Siga-me no Twitter.
Artigo escrito por Nazar Ilamanov e traduzido por Marcelo Panegali.
Oldest comments (0)