WEB3DEV

Cover image for Apócrifos do Ethereum
Adriano P. Araujo
Adriano P. Araujo

Posted on

Apócrifos do Ethereum

Trabalhando na evm.storage, muitas vezes nos deparamos com designs de contratos únicos e interessantes. Se puder ser feito, alguém provavelmente tentou fazer. Achamos que essa é uma característica maravilhosa das blockchains descentralizadas, mesmo que às vezes nos cause dores de cabeça na implementação. Neste post, compartilharemos algumas surpresas que encontramos:

· Um contrato "grande"
· Muitos proxies → uma implementação
· Um proxy → muitas implementações
· Layouts sobrepostos
· Variáveis profundamente aninhadas

Um contrato "grande"

O XENCrypto é interessante principalmente porque é grande. Nossos dados mostram que ele tem mais de 190 milhões de gravações de armazenamento! Você pensaria que isso sugeriria que o token tinha muitos detentores, mas descobrimos que apenas 303 mil chaves em balances e 54 mil em allowances foram escritas.

Então, por que ele é tão grande? Há uma dica em como eles definem o projeto: "XEN Crypto é um projeto de mineração social... XEN é categorizado como uma criptomoeda de Prova de Participação (PoP)".

Ao analisar os dados, vemos mais de 10 milhões de chaves (!) usadas no mapeamento userMints. Cada valor nesse mapeamento é uma struct XENCrypto.MintInfo que requer 6 slots, então temos 60 milhões de slots associados no total. Alguns desses userMints contêm membros cujos valores são zero (um exemplo).

Cada struct MintInfo no mapeamento userMints ocupa 6 slots. Imagine 10 milhões desses!

Qual é o mecanismo que impulsiona esse alto volume de mints (cunhagens) ? Mints gratuitos! Exceto pelo gás, é claro. Qualquer pessoa com uma carteira pode enviar uma transação para começar a criar tokens Xen. O usuário define um prazo em dias e pode reivindicar seus tokens criados no final do prazo. A quantidade que um usuário recebe aumenta com o número de dias e diminui com o número de outros usuários criando tokens no mesmo período.

Para completar, também vemos quase 20 mil chaves usadas em userStakes e mais 5 mil usadas em userBurns.

A CoinGecko relata que o XENCrypto em um ponto estava usando quase metade do espaço de bloco Ethereum, com seu impacto no preço do gás contribuindo significativamente para o Ether se tornar uma moeda deflacionária! Aparentemente, agora está sobre a Base! Apesar de tudo isso, o XENCrypto não apresentou problemas para nossa infraestrutura.

Aliás, se você gostou dos dados sobre o número de chaves em um mapeamento, junte-se ao nosso grupo do Telegram e grite conosco para colocar contagens de chaves de mapeamento no produto. Ainda não priorizamos isso - eu imploro à nossa equipe de engenharia por isso a cada ciclo, mas ninguém me ouve.

Muitos proxies → uma implementação

A Ambisafe é uma provedora de infraestrutura blockchain, e seu trabalho inclui carteiras inteligentes de contratos: 0x0b...d0 é um contrato proxy não verificado com implementação não verificada 0xc3...5c. Aqui está o que é notável: detectamos que 0xc3...5c é o endereço de implementação de mais de 545 mil contratos proxy! E há outro contrato de implementação implantado pela Ambisafe, 0x07...16, que tem 540 mil contratos proxy associados.

545 mil contratos proxy compartilham uma única implementação.

Sem saber muito sobre sua implementação, em alto nível faz muito sentido permitir que os usuários implantem carteiras inteligentes de contratos que obtenham toda a sua lógica via DELEGATECALLs em um contrato de implementação compartilhado.

Esse padrão da Ambisafe nos causou pequenas dificuldades na construção da evm.storage. Identificamos pares de proxy-implementação por meio de DELEGATECALLs. Definimos o intervalo relevante do contrato de implementação pelos DELEGATECALLs encontrados primeiro e por último. Escolhemos essa abordagem porque não exige que os contratos sejam verificados ou assuma que o endereço do contrato de implementação esteja escrito em um slot específico no armazenamento do proxy (relacionado: ERC-1967).

Quando começamos a trabalhar na cobertura de proxy, estávamos pensando principalmente nisso como uma abordagem para atualização. Nesse cenário, cada implementação é tipicamente associada a um único proxy, e um proxy pode estar associado a algumas implementações à medida que o desenvolvedor atualiza a lógica em várias versões.

Mas outro padrão comum é usar proxies para otimização de gás. Se você deseja executar a mesma lógica em muitos contratos, é eficiente implantar essa lógica em um único contrato de implementação e, em seguida, DELEGATECALL para ele a partir de muitos proxies, como vemos com a Ambisafe. Isso quebrou nossa abordagem inicial de rastrear pares, pois nossos processos ficaram sobrecarregados ao carregar constantemente grandes listas do banco de dados para a memória, processando verificações duplicadas e gravando-as de volta. Mudar para uma estrutura de lista vinculada para manter os relacionamentos nos permitiu escalar melhor esses relacionamentos muitos para um.

Um proxy → muitas implementações

Um Bot MEV muito social. Eu gostaria de ter tantos amigos assim.

A Ambisafe teve a implementação com mais proxies. Qual é o proxy com mais implementações? Nossos dados sugerem que é este Bot MEV, com 418 contratos de implementação associados (por exemplo). Observe que estou apenas contando DELEGATECALLees distintos - esses provavelmente não representam relacionamentos padrão de proxy-implementação. Treze deles tiveram intervalos de implementação de menos de dez blocos.

Edição:
banteg apontou que este contrato pode ser alguma variante da família ds-proxy, popularizada pelo MakerDAO. Fizemos alguma análise estática para confirmar. Não é uma cópia direta, pois foi compilado com uma versão mais recente do solc (0.8.9) e contém algumas funções não encontradas no original:

{  
"1cff79cd": "execute(address,bytes)",  
"1f6a1eb9": "execute(bytes,bytes)",  
"60c7d295": "cache()",  
"65fae35e": "rely(address)",  
"78e111f6": "Unresolved",  
"948f5076": "setCache(address)",  
"97645e37": "Unresolved",  
"9c52a7f1": "deny(address)",  
"a90e8731": "Unresolved",  
"bf353dbb": "wards(address)",  
"c9892a5f": "Unresolved"  
}
Enter fullscreen mode Exit fullscreen mode

Mas definitivamente parece ser alguma variante. Obrigado!

Atualmente, não há muito mais que possamos dizer, já que os contratos não são verificados, mas estamos trabalhando na geração de layouts de armazenamento diretamente a partir do bytecode, o que fornecerá mais visibilidade. Aqui está um teaser:

Layouts de armazenamento inferidos, em breve nas páginas de contratos não verificados perto de você!

Uma coisa que vemos é que os intervalos relevantes para os contratos de implementação estão se sobrepondo, o que significa que o proxy está delegando lógica para várias implementações ao mesmo tempo. Essa lógica também está presente no ERC-2535: Diamonds, Multi-Facet Proxy. A ideia é que um determinado contrato proxy (um "diamante") possa usar lógica em vários contratos de implementação ("facetas"), cada um dos quais é responsável por uma parte separável da lógica e conjunto de slots no armazenamento do proxy.

Atualmente, apenas indexamos pares de proxy-implementação com base em seus intervalos de bloco, ou seja, a duração do primeiro bloco em que observamos um DELEGATECALL até o último. Para fornecer cobertura completa para Diamonds, também precisaremos registrar os slots relevantes para cada um. Estamos planejando este trabalho, mas ainda não começamos.

Layouts sobrepostos

Um padrão comum que observamos em layouts de proxy não vazios é que o layout do proxy é idêntico ao da implementação (por exemplo, PositionToken). Estamos pensando em adicionar alguma indicação visual para deixar claro de qual layout cada entrada no mapa de armazenamento unificado (e na tabela) está vindo. No momento, pode parecer que há dados duplicados nesses casos.

Um padrão semelhante é ter um layout que contenha variáveis de gap explícitas nos slots governados pelo layout do outro contrato, como vemos com este proxy Spice Finance. Outra "característica" que descobrimos é que um único contrato pode atribuir o mesmo nome a diferentes variáveis, desde que estejam em escopos diferentes, portanto, você pode ter muitas dessas variáveis de gap idênticas.

Também encontramos alguns usos mais criativos. O EternalStorageProxy coloca um endereço para _upgradeabilityOwner no slot 0x00…00, embora sua implementação use o mesmo slot para um mapeamento. Essa sobrecarga funciona porque o valor realmente usado no slot do mapeamento é sem sentido; normalmente é deixado como zeros, mas não há motivo para não servir outro propósito. Eles fazem um truque semelhante nos slots 0x00…03 a 0x00…05, exceto que, nesses casos, o proxy e a implementação concordam que é um mapeamento, mas discordam sobre qual mapeamento é. Claro, isso poderia causar colisões se alguma chave fosse usada em ambos os mapeamentos.

O uintStorage de um homem é o bytesStorage de outro.

Variáveis profundamente aninhadas

Isso não é apócrifo, por assim dizer, já que frequentemente nos deparamos com exemplos como esses, mas adoramos variáveis profundamente aninhadas! Confira a permissão no Uniswap's Permit 2: um mapeamento triplo aninhado com uma estrutura de três membros no final do arco-íris! Lindo!

Aninhe esses mapeamentos!

Parabéns aos colegas da smlXL (e @banteg) que ajudaram com este post. Maldições àqueles que continuam se recusando a priorizar contagens de chaves de mapeamento.


Este artigo foi escrito por Shane Auerbach e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Latest comments (0)