WEB3DEV

Cover image for Porque a Art Blocks usa JavaScript em seu Contrato Inteligente
Panegali
Panegali

Posted on

Porque a Art Blocks usa JavaScript em seu Contrato Inteligente

Desconstruindo o Contrato Inteligente da Art Blocks

1

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:

2

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;
}
Enter fullscreen mode Exit fullscreen mode
  • name e symbol 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 semelhante setApprovalForAll (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:

  1. um contrato de implementação do padrão ERC-721
  2. 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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • 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);
}
Enter fullscreen mode Exit fullscreen mode
  • Embora não faça parte do padrão ERC-721, a implementação ERC-721 do OpenZeppelin inclui funções mint e burn:
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);
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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];
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Deixe-me explicar as últimas 4 linhas:

  • tokenId é a ID de um NFT e projectId é 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 valor hash é calculado durante a função mint:

3

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;
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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:

5

Você pode obter essas informações do Etherscan. Basta rolar para baixo até "tokenURI". Se navegarmos para este caminho HTTP, obtemos este arquivo JSON:

6

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:

7

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 tokenIdpara 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):

8

Os scripts reais estão em projectScriptByIndex:

9

Os scripts são armazenados como strings simples na estrutura de dados do Projeto:

10

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?

11

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).

12

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.

Latest comments (0)