WEB3DEV

Cover image for Um olhar sobre a busca de dados em Blockchain usando SudoSwap e Multicall
Rafael Ojeda
Rafael Ojeda

Posted on

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

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

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 .

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

Latest comments (0)