Código publicado em uma blockchain EVM é imutável. Isso é uma garantia valiosa para quem usa o contrato e um problema para quem precisa corrigir um defeito ou evoluir uma regra. Os padrões de proxy existem para conciliar as duas coisas, mas trocam um risco por outro: quem pode atualizar o contrato pode, em tese, mudar tudo o que ele faz.
Este artigo explica como os proxies funcionam, onde costumam falhar e quando a melhor decisão é não usá-los.
Como um proxy funciona
Um proxy separa estado e lógica em dois contratos. Os usuários interagem com o proxy, que guarda o armazenamento e o saldo. Cada chamada é repassada com delegatecall para um contrato de implementação, que contém o código. Como delegatecall executa o código da implementação no contexto do proxy, as variáveis lidas e escritas são as do proxy.
Atualizar significa apontar o proxy para uma nova implementação. O endereço que os usuários conhecem não muda, e o estado é preservado.
O endereço da implementação fica gravado em uma posição de armazenamento padronizada pela EIP-1967, escolhida para não colidir com as variáveis do próprio contrato.
Transparent ou UUPS
Os dois padrões mais usados diferem em onde fica a função de upgrade:
| Transparent proxy | UUPS (EIP-1822) | |
|---|---|---|
| Lógica de upgrade | No proxy | Na implementação |
| Custo por chamada | Maior (o proxy verifica quem chama) | Menor |
| Risco característico | Admin separado, mais peças | Uma implementação sem a função de upgrade trava o contrato para sempre |
Em UUPS, cada nova implementação precisa continuar herdando a lógica de upgrade. Esquecer isso em uma versão torna o contrato permanentemente não atualizável. Ferramentas de validação de upgrade verificam esse caso antes do deploy.
O erro mais caro: o layout de armazenamento
O proxy guarda as variáveis por posição, não por nome. Se a versão 2 da implementação reordena, remove ou muda o tipo de uma variável existente, ela passa a ler e escrever as posições erradas. O resultado é corrupção silenciosa de dados: saldos atribuídos à variável errada, permissões trocadas.
As regras práticas:
- Nunca remova, reordene ou mude o tipo de variáveis já publicadas.
- Adicione novas variáveis apenas no final, ou use armazenamento com namespace (ERC-7201), que isola cada módulo em uma região própria.
- Em contratos herdados, reserve espaço com lacunas de armazenamento, para que a classe base possa ganhar variáveis sem deslocar as das classes filhas.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
contract VaultV1 is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // the implementation itself can never be initialized
}
function initialize(address upgrader) external initializer {
__AccessControl_init();
_grantRole(UPGRADER_ROLE, upgrader); // a timelock, not a personal wallet
}
function _authorizeUpgrade(address) internal override onlyRole(UPGRADER_ROLE) {}
}Inicializadores no lugar de construtores
O construtor da implementação roda no contexto da própria implementação, não do proxy. Por isso, contratos atualizáveis usam uma função initialize, chamada uma única vez através do proxy.
Dois cuidados evitam incidentes conhecidos:
- A função de inicialização deve ser protegida para rodar uma única vez.
- A própria implementação deve ter os inicializadores desabilitados no construtor. Caso contrário, qualquer pessoa pode inicializá-la diretamente e assumir papéis administrativos nela.
Governança: quem pode atualizar
Um contrato atualizável é tão confiável quanto a chave que autoriza o upgrade. Uma única carteira pessoal com esse poder é um ponto único de falha, técnico e humano.
Práticas comuns:
- Multisig para propor upgrades, exigindo várias assinaturas independentes.
- Timelock entre a proposta e a execução, dando aos usuários tempo para revisar a mudança e, se discordarem, sair.
- Revisão da nova implementação antes da execução, com o mesmo rigor do contrato original, incluindo auditoria independente quando o contrato custodia valor.
Quando não usar proxy
Atualização não é obrigatória. Contratos imutáveis são mais simples de auditar e oferecem uma garantia forte: ninguém pode mudar as regras depois. Em muitos casos, isso é exatamente o que os usuários precisam.
Alternativas à atualização incluem publicar uma nova versão e oferecer migração voluntária, ou manter o núcleo imutável e colocar regras que mudam com frequência em módulos separados e substituíveis.
A pergunta útil não é "o contrato deve ser atualizável?", e sim "quem deve poder mudar o quê, e com que garantias para os usuários?".
Resumo
Proxies permitem corrigir e evoluir contratos sem trocar de endereço, ao custo de novos riscos: corrupção de armazenamento, inicialização indevida e concentração de poder em quem autoriza o upgrade. Validar o layout de armazenamento antes de cada deploy, desabilitar inicializadores na implementação, proteger o upgrade com multisig e timelock, e considerar seriamente a imutabilidade, mantém esses riscos sob controle. Nenhuma dessas práticas substitui uma auditoria independente quando há valor em jogo.
Leituras relacionadas
- Engenharia blockchain · 4 min de leitura
- IA aplicada · 3 min de leitura
- Fintech · 3 min de leitura