Ir para o conteúdo

Colisão de armazenamento do proxy: como upgrades corrompem o estado

O proxy mantém o estado enquanto o código de implementação muda. Entenda como layouts incompatíveis sobrescrevem saldos, proprietários e controles e como validar um upgrade.

Atualizado

Somente para fins educacionais; não constitui aconselhamento de investimento. Investimentos podem causar perdas.

Resposta direta

Uma colisão de armazenamento do proxy ocorre quando o código de implementação lê ou grava um slot do proxy com significado diferente daquele definido pelo layout que criou o estado existente. Um upgrade pode fazer um saldo parecer um endereço, apagar o proprietário, corromper o slot-base de um mapping ou sobrescrever dados de controle do upgrade.

Com delegatecall, o bytecode da implementação é executado no contexto do proxy: armazenamento, saldo e address(this) pertencem ao proxy. Nomes de variáveis não ficam registrados on-chain; a EVM segue apenas o slot e o deslocamento em bytes calculados pelo novo código.

Por isso, um upgrade precisa preservar o layout implantado, não apenas compilar ou expor as mesmas funções. Antes da autorização, compare os layouts gerados pelo compilador com a versão exata implantada.

Como funciona

Normalmente, Solidity posiciona variáveis de estado a partir do slot 0, na ordem de declaração após a linearização C3 da herança. Valores menores que 32 bytes podem compartilhar um slot; structs e arrays têm regras adicionais, enquanto mappings e arrays dinâmicos derivam seus dados de um slot-base. Se esse slot mudar, a localização dos dados derivados também muda.

Há quatro limites de colisão distintos a auditar:

  • Proxy versus implementação: campos do proxy, como implementação e administrador, não podem ocupar slots usados pelo estado da aplicação. O ERC-1967 define slots padronizados, evitados pelo compilador, para implementação, beacon e administrador.
  • Implementação antiga versus nova: variáveis existentes precisam manter slots, deslocamentos e tipos compatíveis. Acrescentar uma variável no fim pode ser seguro; inserir, reordenar, remover ou mudar o tipo pode reinterpretar palavras existentes.
  • Herança: adicionar estado a um contrato-base ou mudar a ordem de herança pode deslocar o armazenamento dos descendentes mesmo sem alterar o código-fonte deles.
  • Armazenamento reservado ou com namespace: um gap usado corretamente reserva espaço para um contrato-base, e namespaces no estilo ERC-7201 isolam layouts. Nenhuma técnica permite mudanças arbitrárias dentro de um layout existente.

Exemplo

Suponha que a versão 1 tenha este layout:

uint256 totalAssets; // slot 0
address owner;       // slot 1

A versão 2 insere incorretamente uma variável no início:

bool paused;         // slot 0, offset 0
uint256 totalAssets; // slot 1
address owner;       // slot 2

Após o upgrade, paused lê o byte inferior do antigo totalAssets, o novo totalAssets lê a palavra do antigo owner como inteiro e owner lê o que já estava no slot 2, geralmente zero. As palavras originais continuam armazenadas, mas o novo código lhes atribui outros significados. Uma transação pode ter sucesso enquanto aplica autorização ou contabilidade a interpretações corrompidas.

Riscos e verificações de upgrade

  • Gere o layout de armazenamento das duas implementações e compare slot, deslocamento, tipo e herança com o contrato de referência realmente implantado.
  • Em um layout linear convencional, adicione variáveis somente no fim. Não reordene campos, mude tipos, remova e reutilize nem altere contratos-base sem provar compatibilidade.
  • Ao consumir um gap, reduza-o pelo número exato de slots reservados usados. Com namespaces, mantenha identificadores únicos e valide mudanças dentro de cada namespace existente.
  • Não trate o ERC-1967 como proteção completa. Ele separa metadados do proxy dos slots da aplicação atribuídos pelo compilador, mas não torna compatíveis dois layouts de implementação.
  • Teste o upgrade e qualquer reinicializador em um fork ou snapshot de estado. Verifique antes e depois proprietários, funções, saldos, allowances, entradas de mappings, pausa, slots de implementação e controles de reversão ou emergência.

Se um upgrade já pode ter causado uma colisão, interrompa novos upgrades e chamadas que alterem estado quando a governança permitir. Preserve o número do bloco anterior e o bytecode, compare o armazenamento bruto dos slots afetados e peça a especialistas um plano de migração com revisão independente. Repetir upgrades sem um mapa de armazenamento comprovado pode destruir mais estado recuperável.

Equívocos comuns

  • “Os nomes não mudaram, então o layout é seguro.” Nomes não determinam posições. Tipos, ordem, empacotamento, herança e namespaces determinam.
  • “Excluir uma variável libera seu slot.” O armazenamento do proxy persiste. Reutilizá-lo atribui novo significado à palavra antiga, salvo se uma migração revisada a apagar ou transformar.
  • “Uma transação de teste bem-sucedida prova compatibilidade.” Ela pode tocar poucos slots. A validação e os testes de diferença de estado devem cobrir campos privilegiados, valores empacotados, mappings, arrays e armazenamento herdado.

Tópicos relacionados

Fontes

Navegação

Pesquisar na wiki...