Skip to content

Um olhar sobre a busca de dados em Blockchain usando SudoSwap e Multicall

Um olhar sobre a busca de dados em Blockchain usando SudoSwap e Multicall

#Um olhar sobre a busca de dados em Blockchain usando SudoSwap e Multicall

Lidando com dados da blockchain, fornecedores de nós e contratos inteligentes utilizando técnicas avançadas

Image description

Durante bastante tempo, fiquei fascinado pelo SudoSwap. Para os não familiarizados com o projeto Sudo, o seu objetivo é criar uma experiência semelhante à de DEXs tal como Uniswap, Curve, e Sushiswap. Pode-se comprar e vender NFTs como se fizessem parte de um par de tokens no Uniswap. Pode comprar ou vender NFTs apenas interagindo com os pools da AMM e obter instantaneamente o seu desejo. Este design muito inteligente e simples com muito sucesso em DEXs bem conhecidas é interessante, por isso decidi aprofundar mais no assunto.

O caso de uso imediato que me ocorreu, para além das sofisticadas Defi primitivas que se podem construir em sudoswap, foi uma melhor arbitragem NFT. Quando o MEV entra em jogo, pode-se esperar um volume de comércio e utilização cada vez mais significativo. Neste momento, o sudoswap está funcionando muito bem, com mais de 35.000 pools e 200.000 negócios com nft. Espero que o sudoswap cresça em grande escala no futuro.

Image description

Análise Dune: https://dune.com/0xRob/sudoamm

Seria uma boa experiência de aprendizagem sujar as minhas mãos e tentar criar uma base de dados que pudesse ser útil para encontrar oportunidades de arbitragem (pelo menos conceitualmente), mas que logo conduziu a uma típica toca de coelho empilhada na web3

Em que consistirá esta base de dados? Muito simples, Uma tabela com o endereço do pool e algumas propriedades fixas importantes do pool, tais como a colecção NFT (endereço), Delta (o efeito de uma venda ou de uma compra sobre o preço) e endereço do token ERC20 (embora não totalmente suportado no sudoswap). Decidi torná-la a mais simples, por isso vamos nos concentrar apenas no endereço NFT e nos pools a ele associados.

Como mencionei anteriormente, o sudoswap tem uma estrutura semelhante à das DEXs conhecidas, tem um contrato de Fábrica que cria uma nova instância de pool de pares, e depois o novo pool em si tem algumas propriedades se quisermos obter todos os pools e os seus respectivos dados de nft precisaremos de duas etapas

  1. Obter todos os endereços de pools sobre sudoswap, o que pode ser feito utilizando eventos

  2. Obter o endereço de colecção NFT de cada pool, o que pode ser feito através de uma chamada de função básica apenas de leitura

Para o primeiro passo, precisamos compreender como vamos obter de forma confiável (utilizando apenas dados na chain) mais de 35k de endereços de pool. Primeiro, precisamos verificar o contrato de fábrica através do etherscan. Uma prática bem conhecida no desenvolvimento de contratos inteligentes é utilizar eventos para indicar que a função de chamada foi bem sucedida e que algumas mudanças na chain foram realizadas. Olhando para o código do contrato de fábrica, podemos ver uma lista de eventos:

Image description

O novo evento de pares é uma combinação perfeita. Portanto, para conseguirmos todos os eventos, precisamos ir buscar todos os eventos de "novos pares". Vamos escrever o código e executá-lo:

const { ethers } = require("ethers");
const { Interface } = require("ethers/lib/utils");
const fs = require("fs");

const interface1 = new Interface(["event NewPair (address poolAddress)"]);
const hexToDecimal = (hex) => parseInt(hex, 16);

async function Create_Index() {
  const provider = new ethers.providers.AlchemyProvider(
    "homestead",
    "API_KEY"
  );

  const addr = "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4"; // Sudo Factory address

  const Factory = new ethers.Contract(addr, interface1, provider);

  var first_block = 14645816; // SudoSwap Factory deployment's block

  const final_block = await provider.getBlockNumber();

  res = await Factory.queryFilter("NewPair", first_block, final_block);
}

Create_Index();

Você precisará adicionar o pacote Ethers.js e criar um fornecedor de nós.Estou usando Alchemy, e pela minha experiência, seu plano gratuito é muito vantajoso. Você também pode criar seu próprio nó, mas pode ser um exagero. Nós executamos o código e...

OOPS, temos um erro no servidor:

Tamanho de resposta dos logs excedido. Você pode fazer solicitações eth_getLogs com um intervalo de blocos de até 2K e sem limite no tamanho da resposta, ou pode solicitar qualquer intervalo de blocos com um limite de 10K logs na resposta. Com base em seus parâmetros e no limite do tamanho de resposta, esta faixa de blocos deve funcionar: [0xdf7a38, 0xea0fe0]

Portanto, aparentemente, não podemos ter os logs inteiros (mais de 35k de eventos) em uma única chamada. Mas temos duas soluções que podemos deduzir da resposta do servidor

  1. percorrer toda a faixa de blocos em 2000 intervalos

  2. usar a faixa exata de blocos que nos dá 10k de logs usando apenas 4 chamadas API

Vamos tentar a primeira solução. Se adicionarmos o código e o executarmos, perceberemos muito rapidamente um problema.

for(first_block; first_block<= final_block; first_block +=2000){

    res = await Factory.queryFilter("NewPair", first_block, first_block+2000);
    arr.push(res);
 }

Se nossa gama de blocos começa em 14.645.816 e termina após 15.700.000, temos mais de 1 milhão de blocos. Teremos que fazer mais de 500 iterações, o que significa mais de 500 chamadas API para nosso fornecedor. Isto nos custará muito em termos de unidades de computação e também levará alguns minutos devido ao rendimento limitado do fornecedor do nó.

Para a segunda solução, obviamente podemos implementar uma função de ajuda que calculará a faixa exata de blocos de 10k de logs, mas é bastante complicada e exigirá uma quantidade razoável de chamadas API mesmo com alguns truques que usamos com base na natureza e no código do contrato de fábrica, podemos usar seu nonce, mas também podemos intencionalmente "falhar" nossa chamada para obter do provedor do nó a faixa de blocos apropriada:

const { ethers } = require("ethers");
const { Interface } = require("ethers/lib/utils");
const fs = require("fs");
const interface1 = new Interface(["event NewPair (address poolAddress)"]);
const hexToDecimal = (hex) => parseInt(hex, 16);

async function Create_Index() {
  const provider = new ethers.providers.AlchemyProvider(
    "homestead",
    "API_KEY"
  );
  const addr = "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4";// Sudo Factory address
  const addr1 = "0x16F71D593bc6446a16eF84551cF8D76ff5973db1";
  const Factory = new ethers.Contract(addr, interface1, provider);
  var events = [];
  var first_block = 14645816; //Bloco de implantação da fábrica SudoSwap 
  var last_block = first_block;
  const final_block = await provider.getBlockNumber();

  //Obtendo todos os eventos do "novo par" em quantidade mínima de chamadas API
  while (first_block <= final_block) {
    try {
      // Falha em cada chamada até a última
      if (
        events.push(
          (res = await Factory.queryFilter("NewPair", last_block, final_block))
        )
      ) {
        console.log("-------------------------------------------------------");
        console.log(
          `fetching data from block ${last_block} to block ${final_block} ...`
        );
        console.log("-------------------------------------------------------");
        break;
      }

      // Usando a resposta de erro do servidor da Alchemy para determinar exatamente a faixa de blocos para a quantidade máxima de 10K logs
    } catch (e) {
      const text = `${e}`;
      console.log("-------------------------------------------------------");
      // corte de texto básico codificado com conversão de string hexadecimal para decimal
      first_block = hexToDecimal(text.slice(397, 405));
      last_block = hexToDecimal(text.slice(407, 415));
      console.log(
        `fetching data from block ${first_block} to block ${last_block} ...`
      );

      events.push(
        await Factory.queryFilter("NewPair", first_block, last_block)
      );
    }
  }
  events = events.flat(); 
  
}

Create_Index();
  1. primeiro, criamos uma matriz de eventos que irá armazenar todos os dados

  2. Em seguida, usamos um padrão de try-catch para processar a resposta do servidor e obter o intervalo exato de blocos

  3. Devemos esperar que cada chamada dentro do bloco de tentativas falhe até a última chamada, que deve ser válida.

  4. obtemos a faixa de blocos através de um corte de texto muito simples e codificado e conversão de string hexadecimal para decimal para ajudar na legibilidade

  5. A cada chamada em que recebemos uma nova matriz de resultados em uma nova posição na matriz de eventos, precisaremos usar o flat() para obter uma matriz de eventos unificada

Isto resulta em uma execução bastante rápida e eficiente de mais de 500 chamadas API para menos de 10. Cada evento tem este aspecto, podemos obter o endereço do pool usando eventos[i].args.poolAddress:

Image description

Agora temos ~35k de endereços. Precisamos obter para cada um desses pools o endereço de coleta do nft. Se olharmos para o código do LSSVMPair, podemos ver que na linha 394, temos uma função básica de leitura que nos dá o endereço de coleta do NFT. Esta é a única função que precisamos especificar em nosso objeto ABI.

function nft() public pure returns (IERC721 _nft)

então nosso objeto de contrato e chamada deve se parecer com isto:

nft = new ethers.Contract(events[i].args.poolAddress, abi, provider ).nft();

Mas o mesmo problema surge se temos contratos de 35k e precisamos chamar a função nft() em cada um deles. O resultado também são chamadas API de 35k. Você pode executar e ver por si mesmo. Para mim, levou mais de 3 horas para ser concluído. Então, qual será a solução? Obviamente, estamos lidando aqui com chamadas de função e não eventos, portanto, a abordagem que usamos anteriormente não é aplicada aqui. Uma nova abordagem MULTICALL pode ser útil. …

Multicall é um contrato implantado na chain que permite a agregação de chamadas contratuais de tal forma que você pode combinar algumas chamadas em apenas 1 chamada API. Há uma série de implementações e pacotes de multicall: Makerdao multicall.js com MULTICALL3 é muito otimizado e flexível, também ethereum-multicall (Uniswap também tem um), mas eu achei 0xsequence multicall muito conveniente para nossos propósitos, se você quiser mergulhar mais fundo, você pode verificar o contrato de multicall aqui.

Note que, embora não haja um limite fixo para o número de chamadas que você pode incluir em um lote, o uso de lotes muito grandes pode levar a comportamentos e erros inesperados. Olhando para as melhores práticas da alchemy

** Ao enviar solicitações de lotes, visar lotes abaixo de 50 solicitações por chamada. **

Podemos usar uma solução muito conservadora e cautelosa, e ainda podemos obter uma melhoria de 50x no tempo e nas chamadas API. De mais de 35k a cerca de 700 chamadas e uma média de 30 ms de tempo de resposta para cada uma, todo o processo deve levar menos de um minuto.

o código final é:

const { ethers } = require("ethers");
const { Interface } = require("ethers/lib/utils");
const fs = require("fs");
const { providers } = require("@0xsequence/multicall");

const interface1 = new Interface(["event NewPair (address poolAddress)"]);
const abi = ["function nft() public view returns (address _nft)"];
const hexToDecimal = (hex) => parseInt(hex, 16);

async function Create_Index() {
  const provider = new ethers.providers.AlchemyProvider(
    "homestead",
    "API_KEY"
  );
  // Segundo fornecedor a utilizar apenas para pedidos de lotes
  const provider1 = new providers.MulticallProvider(
    new ethers.providers.AlchemyProvider(
      "homestead",
      "API_KEY"
    )
  );

  const addr = "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4";// Sudo Factory address
  const Factory = new ethers.Contract(addr, interface1, provider);
  var events = [];
  var first_block = 14645816; // Bloco de implantação da fábrica SudoSwap 
  var last_block = first_block;
  const final_block = await provider.getBlockNumber();

  //Obter todos os eventos do "novo par" em quantidade mínima de chamadas API
  while (first_block <= final_block) {
    try {
      // Falha em cada chamada até a última
      if (
        events.push(
          (res = await Factory.queryFilter("NewPair", last_block, final_block))
        )
      ) {
        console.log("-------------------------------------------------------");
        console.log(
          `fetching data from block ${last_block} to block ${final_block} ...`
        );
        console.log("-------------------------------------------------------");
        break;
      }

      // Usando a resposta de erro do servidor da Alchemy para determinar exatamente a faixa de blocos para a quantidade máxima de 10K logs
    } catch (e) {
      const text = `${e}`;
      console.log("-------------------------------------------------------");
      //Corte de texto básico codificado com conversão de string hexadecimal para decimal
      first_block = hexToDecimal(text.slice(397, 405));
      last_block = hexToDecimal(text.slice(407, 415));
      console.log(
        `fetching data from block ${first_block} to block ${last_block} ...`
      );

      events.push(
        await Factory.queryFilter("NewPair", first_block, last_block)
      );
    }
  }
  events = events.flat();

  var calls = [];
  //Construindo um conjunto de promessas de chamadas contratuais
  for (i = 0; i < events.length; i++) {
    const Pair = new ethers.Contract(
      events[i].args.poolAddress,
      abi,
      provider1
    ).nft();
    calls.push(Pair);
  }
  console.log("Fetching collections data ...");

  //Usando Multicall
  var res = await Promise.all(calls);
  var final = [];

  for (i = 0; i < events.length; i++) {
    final.push({ addr: events[i].args.poolAddress, nft: res[i] });
  }
  const t = JSON.stringify(final);
  fs.writeFile("Pairs.json", t, function (err, result) {
    if (err) console.log("error", err);
  });
  console.log("Finished");
}
Create_Index();
  1. Estamos usando o pacote mutlicall, a primeira parte (0-63 linhas) é idêntica ao código compartilhado anteriormente com algumas modificações, adicionamos ABI para as funções ** nft() ** somente leitura e criamos 2 provedores, um provedor regular para obter os eventos e um segundo que é um provedor multicall para as chamadas do contrato

  2. a partir da linha 65, criamos uma matriz de chamadas que deve conter todas as chamadas contratadas de 35k sem executá-las (ou seja, enviá-las como chamadas API no momento em que o programa chega a contract.nft() )

  3. usamos então promise.all() na matriz de chamadas que unifica todas as promessas de chamadas contratuais a uma promessa

  4. então procedemos à criação de um conjunto de objetos de dados do pool chamados final

  5. finalmente, criamos um arquivo JSON com todos os dados dos pares

                                                                    …
    

Alguns pontos importantes

Lidar com dados na chain é muito desafiador, os dados podem parecer muito acessíveis através de serviços como o Etherscan, mas está longe de ser acessível para os POCs, sem mencionar a produção pronta.

Usar fornecedores de nós pode ser uma maneira muito eficiente de superar a rotação de seu próprio nó, mas a produção limitada deve ser levada em conta. Criatividade e soluções prontas devem ser exercitadas para superar as limitações.

No nível infra, os serviços de blockchain não fornecem uma solução razoável para a obtenção e indexação de dados de uso geral. Há um sentimento de que muitas equipes estão usando soluções ad-hoc para dados em blockchain, o que levanta algumas questões sobre a capacidade de lidar com escala intensa em produtos e serviços web3 com uso intensivo de dados.

Há alguns avanços na área, tais como API aprimorada ou NFT API, e empresas como Moralis e The Graph, mas nenhuma delas poderia nos fornecer uma solução fácil. Pesquisando estas soluções, achei-as muito gerais, sem capacidade de modificar para certas necessidades ou muito restritas e fornecendo casos de uso muito específicos. …

O código completo do repo está disponível no GitHub com algumas integrações adicionais para o MongoDB

Este Artigo foi escrito por Natan B e traduzido para o português por Rafael Ojeda

Você encontra o artigo original em inglês aqui