WEB3DEV

Cover image for Mergulho Profundo em Proxies de Contratos Inteligentes: Variantes, CREATE vs. CREATE2 e Considerações de Segurança
Paulo Gio
Paulo Gio

Posted on

Mergulho Profundo em Proxies de Contratos Inteligentes: Variantes, CREATE vs. CREATE2 e Considerações de Segurança

Sumário

  1. Introdução
  2. Vantagens de Usar Proxies
  3. Padrões de Proxy
  4. Opcodes de Implantação: CREATE vs. CREATE2
  5. Vulnerabilidades Comuns
  6. Implantação de um Proxy Usando o OpenZeppelin
  7. Considerações Finais

Introdução

No mundo dos contratos inteligentes da Ethereum, os proxies desempenham um papel crucial na obtenção de atualizabilidade, consistência de endereço e separação de estado e dados da lógica do contrato. Neste artigo, vamos explorar os mecanismos internos dos proxies de contratos inteligentes, seus métodos de implantação usando os opcodes CREATE e CREATE2, vários padrões de proxy e os potenciais riscos de segurança associados à sua implementação.

Um proxy de contrato inteligente consiste em dois componentes principais: o contrato de proxy e o contrato de implementação (lógica). O contrato de proxy é responsável por manter o estado do contrato, o armazenamento de dados e o endereço consistente. Ele encaminha as chamadas de função recebidas para o contrato de implementação, que contém a lógica de negócio real e o bytecode. A chave desse mecanismo de encaminhamento é o opcode delegatecall, que permite que o contrato de proxy delegue a execução de uma função para o contrato de implementação, mantendo seu próprio contexto de estado e dados.

Vantagens de Usar Proxies

Os proxies de contratos inteligentes oferecem várias vantagens, tornando-os uma escolha atrativa para os desenvolvedores que trabalham no ecossistema da Ethereum. Aqui estão alguns benefícios-chave do uso de proxies:

Eficiência de Gás

A implantação de contratos com código de implementação pode ser bem cara em termos de custos de gás. Por exemplo, criar um par na UniswapV2 requer aproximadamente 2,5 milhões de gás, o que equivale a mais de $140 no momento da escrita (30 Gwei e $1900/ETH). No entanto, o uso de um Contrato de Proxy Mínimo (Clone), conforme a EIP-1167, reduz significativamente o consumo de gás. Com cerca de 150.000 gás, o custo cai para um pouco mais de $8 (com o mesmo preço do gás e valor do Ether). Essa eficiência torna os proxies uma escolha mais econômica para os desenvolvedores.

Atualizabilidade

Uma das principais vantagens de usar proxies é sua capacidade de atualização. Essa funcionalidade permite que os desenvolvedores modifiquem ou atualizem a lógica de um contrato sem alterar seu endereço, mantendo assim a consistência no estado e nos dados do contrato durante todo o processo de atualização. A atualizabilidade é crucial para garantir que os contratos inteligentes possam se adaptar a requisitos em mudança ou corrigir possíveis problemas sem comprometer a experiência do usuário ou causar interrupções nas funcionalidades existentes.

Desvantagens e Considerações

Embora os proxies ofereçam várias vantagens, existem algumas desvantagens a serem consideradas. Uma desvantagem notável é o aumento do custo de gás para cada chamada feita por meio de um proxy. Isso ocorre porque o endereço de implementação da lógica subjacente precisa ser lido, e a localização desse endereço pode variar entre diferentes tipos de proxies. Consequentemente, ao realizar uma transação por meio do proxy, o proxy precisa ler o endereço e, em seguida, delegar a chamada, o que aumenta o custo de gás.

Apesar dessa desvantagem, as vantagens de usar proxies no ecossistema da Ethereum geralmente superam as possíveis desvantagens. Os desenvolvedores podem aproveitar a eficiência de gás e a atualizabilidade para criar contratos inteligentes mais flexíveis e econômicos, melhorando assim a experiência geral do usuário.

Inicializador

No mundo dos proxies de contratos inteligentes, os inicializadores desempenham um papel fundamental no processo de configuração. A maioria dos proxies modernos é inicializada em vez de usar construtores, já que os construtores não são compatíveis com contratos de proxy. Para evitar qualquer confusão, é recomendado desabilitar os inicializadores no contrato de implementação, já que eles geralmente não são para uso público em geral.

Usando a biblioteca Initializable do OpenZeppelin, isso é facilmente alcançável. Exemplo de código abaixo:

/// @custom: construtor oz-upgrades-unsafe-allow 
constructor() {
  _disableInitializers();
}
Enter fullscreen mode Exit fullscreen mode

Padrões de Proxy

Ao trabalhar com proxies de contratos inteligentes, é importante entender os diferentes tipos de padrões de proxy disponíveis e as compensações entre eles. Os tipos mais comuns de proxies são:

  1. Clones [EIP-1167]
  2. Universal Upgradeable Proxy Standard (UUPS) [EIP-1967]
  3. Transparent Upgradeable Proxies (Proxies Transparentes Atualizáveis)

Contrato de Proxy Mínimo (Clones) [EIP-1167]

O padrão Clone, definido na EIP-1167, oferece certas vantagens na implantação de contratos inteligentes. Ao contrário dos proxies típicos, os Clones removem várias funcionalidades, como a atualizabilidade, o que resulta em custos de implantação menores. Os Clones não possuem construtores e são implantados com o endereço da lógica subjacente incorporado no bytecode.

Essa abordagem simplificada elimina a necessidade de ler do armazenamento durante cada chamada, pois delega diretamente a chamada. Consequentemente, esse processo eficiente economiza gás e oferece uma solução mais econômica.

Universal Upgradeable Proxy Standard (UUPS) [EIP-1967]

O padrão UUPS, formalmente introduzido como parte da EIP-1822 e incorporando a EIP-1967, é um modelo sofisticado de proxy que otimiza os custos de implantação e o uso de gás. O UUPS se baseia na EIP-1967 para armazenar o endereço de implementação em um slot de armazenamento único dentro do contrato de proxy. Armazenar a lógica de atualização no contrato de implementação, que é um aspecto fundamental do padrão UUPS, reduz efetivamente os custos totais de implantação dos proxies.

Essa abordagem elimina a necessidade de verificar se o chamador é um administrador no Proxy, o que economiza gás para cada chamada. Além disso, evita colisões de função entre o contrato de implementação e a lógica de atualização no proxy. No entanto, esse design aumenta a complexidade do lado da implementação, já que cada versão do contrato de implementação deve incluir a função de atualização.

Os desenvolvedores devem ter cuidado para garantir que o contrato de implementação não se autodestrua (selfdestruct) ou termine em um estado indesejável, pois isso resultará em um proxy inutilizável. O UUPS também oferece a flexibilidade de personalizar o mecanismo de autorização para atualizações, como a incorporação de sistemas de votação ou bloqueios temporizados, proporcionando maior controle sobre o processo de atualização.

Proxies Transparentes Atualizáveis

Os Proxies Transparentes Atualizáveis (TUP), como o UUPS, também incorporam a EIP-1967, fornecendo um padrão de proxy eficiente e simplificado para implantações de contratos inteligentes. A principal distinção entre Proxies Transparentes Atualizáveis e UUPS reside na localização da lógica de atualização. Nos Proxies Transparentes Atualizáveis, a lógica de atualização é armazenada no próprio proxy, garantindo que apenas o administrador do proxy possa iniciar uma atualização.

Este design oferece um nível de segurança, pois o proxy só delegará chamadas se o chamador não for o administrador. Se o chamador for qualquer outro endereço, o proxy sempre delegará a chamada, mitigando assim o risco de conflito de funções para administradores.

Embora os Proxies Transparentes Atualizáveis ofereçam uma solução robusta para atualizações de contratos inteligentes, os desenvolvedores devem considerar as implicações de ter a lógica de atualização dentro do proxy, pois isso pode apresentar desafios em termos de uso de gás e gerenciamento de armazenamento. No entanto, o padrão continua sendo uma escolha popular devido à sua simplicidade e facilidade de implementação.

Comparação de Uso de Gás entre os Tipos de Proxy

Para entender melhor a eficiência dos diferentes tipos de proxy, vamos examinar o uso de gás para chamadas de função e implantação de contratos. A tabela abaixo apresenta a comparação do uso de gás para cada tipo de proxy e uma implementação direta de contrato.

A seguinte demonstração foi feita com um ERC20 como implementação base. Os dados de uso de gás foram obtidos usando uma combinação do hardhat-gas-reporter e dos recibos de transação. O código-fonte pode ser encontrado aqui.

Observação: O UUPS precisou de uma implementação adicional mínima para o ERC20. Sua implementação pode ser encontrada em contracts\FactoryUUPS.sol, no código-fonte.

https://miro.medium.com/v2/resize:fit:640/format:webp/1*gQjYPoSlb2DYlrYt7QZxpg.png

Como mostrado na tabela, o uso de gás varia significativamente entre os três tipos de proxies e a implementação nativa. Os proxies Clone demonstram menor uso de gás para chamadas de função em comparação com os proxies TUP e UUPS. No entanto, eles têm um uso ligeiramente maior de gás para a implantação de contratos em comparação com a abordagem de implementação direta.

Os proxies UUPS têm um uso de gás relativamente menor para chamadas de função e implantações de proxy (createToken) em comparação com os proxies TUP. Os Proxies Transparentes Atualizáveis têm o maior uso de gás tanto para chamadas de função quanto para implantações de proxy entre os três tipos de proxies, com custos um pouco menores para a implantação inicial do contrato de implementação em comparação com o UUPS.

Os desenvolvedores devem considerar essas diferenças de uso de gás ao selecionar um padrão de proxy, pois isso pode ter um impacto significativo no custo geral e na eficiência das implantações e interações de contratos inteligentes.

Opcodes de Implantação: CREATE vs. CREATE2

No contexto da implantação de contratos inteligentes, existem dois métodos principais a serem considerados: CREATE e CREATE2. Cada método possui características únicas que afetam o endereço do contrato resultante e o processo de implantação. Além disso, a escolha do método de implantação pode ter implicações de segurança significativas, como demonstrado pelo ataque Wintermute na Optimism.

CREATE

O método CREATE determina o endereço do contrato com base no nonce do contrato de fábrica. Cada vez que o CREATE é chamado na fábrica, o nonce é incrementado em 1. A fórmula para determinar o endereço do contrato é keccak256(sender, nonce).

CREATE2

Introduzido na EIP-1014 com o fork Constantinopla, o CREATE2 oferece um método alternativo de implantação que permite mais controle e flexibilidade sobre o endereço do contrato resultante. Ao contrário do CREATE, o CREATE2 usa um sal (salt) e o bytecode criptografado com a função hash keccak do contrato como parte do cálculo do endereço. A fórmula para determinar o endereço do contrato é keccak256(0xFF, sender, salt, keccak256(bytecode)).

Como observação adicional, a constante 0xff usada na fórmula garante que os endereços de contrato resultantes gerados pelo CREATE2 não possam colidir com aqueles gerados pelo opcode CREATE. Isso ocorre porque o byte 0xff, quando usado como byte inicial na codificação Recursive-length Prefix (RLP), só seria aplicável a estruturas de dados com vários petabytes de tamanho, o que os contratos Ethereum e as estruturas de dados não alcançam.

Aviso: Com o CREATE2, é possível que um contrato seja implantado no mesmo endereço com um bytecode diferente por meio de um Padrão de Contrato Inteligente Metamórfico. Para saber mais sobre esse padrão e suas implicações, consulte este recurso.

Ataque Wintermute na Optimism

O ataque Wintermute na Optimism serve como um exemplo de como a escolha do método de implantação pode ter consequências graves de segurança. Nesse caso, a Optimism enviou 19 milhões de tokens OP para o proxy do Gnosis Safe da Wintermute na Mainnet (rede principal). No entanto, o proxy do Gnosis Safe não tinha sido implantado na Optimism. O Gnosis Safe na Mainnet foi criado com uma versão mais antiga do contrato ProxyFactory, que usava o opcode CREATE. Um invasor conseguiu reproduzir a implantação da Cópia-mestra (Mastercopy) do Gnosis Safe a partir da Mainnet e, em seguida, implantar lotes de cofres até atingir o cofre da Wintermute.

Leia mais sobre o ataque Wintermute na Optimism aqui.

Em resumo, os desenvolvedores devem considerar os benefícios e as desvantagens de cada método de implantação ao trabalhar com contratos inteligentes. Embora o CREATE ofereça uma abordagem simples e incremental para os endereços dos contratos, ele também apresenta riscos potenciais associados a vulnerabilidades de segurança, como o ataque Wintermute na Optimism. Por outro lado, o CREATE2 oferece mais controle e flexibilidade, o que pode ser benéfico em determinados casos de uso.

Vulnerabilidades Comuns

Ao trabalhar com padrões de proxy, é essencial estar ciente das vulnerabilidades comuns e dos possíveis riscos de segurança. A seguir estão algumas vulnerabilidades comuns e as medidas para mitigá-las.

Proxy Não Inicializado

Uma vulnerabilidade de proxy não inicializado ocorre quando um proxy é implantado sem ser inicializado ou pode ser reinicializado. Essa vulnerabilidade permite que um invasor inicialize o proxy com parâmetros não pretendidos, o que pode levar a consequências indesejadas ou perda de fundos.

Medidas de Mitigação:

  • Inicialize o proxy na mesma transação da implantação.
  • Certifique-se de que a função de inicialização possa ser chamada apenas uma vez. Uma abordagem para alcançar isso é usar o modificador initializer da biblioteca Initializable do OpenZeppelin.

Vulnerabilidades com Selfdestruct

Várias vulnerabilidades podem surgir quando um contrato usa a funcionalidade selfdestruct:

  • Se o contrato de implementação contém o selfdestruct e é ativado pelo proxy, o proxy será destruído.
  • Se o contrato de implementação for destruído, todos os clones criados com essa implementação deixarão de funcionar. Além disso, se o contrato de implementação foi implantado usando o CREATE2, há uma possibilidade de que o contrato de implementação seja substituído por um contrato malicioso.

Medidas de mitigação:

  • Certifique-se de que o contrato de implementação não contenha a funcionalidade selfdestruct, a menos que esteja protegido e a destruição do proxy seja intencional.
  • Verifique se o contrato de implementação não faz delegatecall para um contrato externo que contenha o selfdestruct. Se um delegatecall for encontrado no contrato externo, continue o processo de verificação até que nenhuma funcionalidade desse tipo seja encontrada.

É importante observar que essa vulnerabilidade pode não ser um problema no futuro devido à desativação do selfdestruct no EIP-4758. Além disso, uma exceção a essa vulnerabilidade existe quando se usa um contrato metamórfico. Para saber mais sobre contratos metamórficos, leia mais sobre isso aqui.

Implantando um Proxy usando OpenZeppelin

Proxies são frequentemente implantados com uma fábrica de contratos. Nesta seção, várias demonstrações mostram como implantar proxies de forma não determinística usando o opcode CREATE e de forma determinística usando o opcode CREATE2 com o OpenZeppelin, usando um ERC-20 como implementação. Todo o código-fonte pode ser encontrado aqui.

Implantando um Proxy de forma não determinística (opcode CREATE)

Tipicamente, a implantação de um proxy de forma não determinística é feita simplesmente criando um novo contrato.

Exemplo de código para implantar um TransparentUpgradeableProxy com o opcode CREATE:

function createToken(
   string calldata name,
   string calldata symbol,
   uint256 initialSupply,
   address owner
) external returns (address) {
   TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
       tokenImplementation,
       msg.sender,
       ""
   );
   ERC20PresetFixedSupplyUpgradeable(address(proxy)).initialize(
       name,
       symbol,
       initialSupply,
       owner
   );
   return address(proxy);
}
Enter fullscreen mode Exit fullscreen mode

Ao implantar um proxy com o padrão UUPS, o contrato de implementação deve ser modificado para incluir a lógica de atualização. No exemplo abaixo, criamos um token ERC20 compatível com UUPS estendendo o contrato ERC20PresetFixedSupplyUpgradeable com UUPSUpgradeable e OwnableUpgradeable. A função _authorizeUpgrade é substituída para restringir as atualizações ao proprietário do contrato.

contract UUPSCompatibleERC20 is
   ERC20PresetFixedSupplyUpgradeable,
   UUPSUpgradeable,
   OwnableUpgradeable
{
   function initialize(
       string memory name,
       string memory symbol,
       uint256 initialSupply,
       address owner
   ) public virtual override initializer {
       __ERC20PresetFixedSupply_init(name, symbol, initialSupply, owner);
       __Ownable_init();
   }

   function _authorizeUpgrade(address) internal override onlyOwner {}
}
Enter fullscreen mode Exit fullscreen mode

Além disso, é possível verificar se a nova implementação do ERC20 é compatível com UUPS usando o plugin @openzeppelin/hardhat-upgrades. No exemplo de código abaixo, demonstramos como implantar um proxy compatível com UUPS usando o Hardhat:

const UUPSCompatibleERC20 = await hre.ethers.getContractFactory("UUPSCompatibleERC20");

const proxy = await hre.upgrades.deployProxy(
   UUPSCompatibleERC20,
   ["Name", "Symbol", 1000, this.accounts[0].address],
   { kind: "uups" }, // <- Esta linha é necessária para a verificação de compatibilidade
);
Enter fullscreen mode Exit fullscreen mode

Usando o método de clonagem do OpenZeppelin para implantar um proxy com o opcode CREATE:

function createToken(
   string calldata name,
   string calldata symbol,
   uint256 initialSupply
) external returns (address) {
   address clone = Clones.clone(tokenImplementation);
   ERC20PresetFixedSupplyUpgradeable(clone).initialize(
       name,
       symbol,
       initialSupply,
       msg.sender
   );
   return clone;
}
Enter fullscreen mode Exit fullscreen mode

Implantando um proxy de forma determinística (opcode CREATE2)

Ao implantar um proxy de forma determinística usando o opcode CREATE2, o processo geralmente envolve gerar um novo contrato que inclui um parâmetro salt. O código de implantação para essa abordagem é bastante semelhante ao método não determinístico, com a adição de {salt: _salt}. Aqui está um exemplo:

Proxy newProxy = new Proxy{salt: _salt}(tokenImplementation);
Enter fullscreen mode Exit fullscreen mode

Usando o método de clonagem do OpenZeppelin para implantar um proxy com o opcode CREATE2:

address clone = Clones.cloneDeterministic(implementationAddress, salt);
Enter fullscreen mode Exit fullscreen mode

Para evitar ataques de repetição em outra cadeia, é aconselhável usar um novo sal derivado do sal antigo e dados exclusivos, como as entradas ou um hash de dados exclusivo. Por exemplo:

bytes20 newSalt = bytes20(keccak256(abi.encodePacked(_initializerData, _salt)));
Enter fullscreen mode Exit fullscreen mode

Palavras Finais

Obrigado por dedicar tempo para explorar os diferentes padrões de proxy, métodos de implantação e vulnerabilidades associadas no mundo do desenvolvimento de contratos inteligentes comigo. Espero que este artigo tenha fornecido informações valiosas e um entendimento mais profundo do assunto. À medida que o ecossistema das blockchains continua a evoluir, estar informado e conhecer esses padrões será crucial. Agradeço seu tempo e interesse, e ficaria grato por qualquer feedback que você possa ter. Além disso, sinta-se à vontade para sugerir tópicos que você gostaria que eu abordasse em futuros artigos.

Observação: Todo o código-fonte deste artigo pode ser encontrado aqui: https://github.com/ljz3/proxies-gas-usage

Leituras adicionais:

Referências

  1. “Proxies.” OpenZeppelin Docs. Accessed April 17, 2023. https://docs.openzeppelin.com/contracts/4.x/api/proxy.
  2. Peter Murray (@yarrumretep), Nate Welch (@flygoing). “ERC-1167: Minimal Proxy Contract.” Ethereum Improvement Proposals, June 22, 2018. https://eips.ethereum.org/EIPS/eip-1167.
  3. Santiago Palladino (@spalladino), Francisco Giordano (@frangio). “ERC-1967: Proxy Storage Slots.” Ethereum Improvement Proposals, April 24, 2019. https://eips.ethereum.org/EIPS/eip-1967.
  4. Gabriel Barros, Patrick Gallagher. “ERC-1822: Universal Upgradeable Proxy Standard (UUPS) [Draft].” Ethereum Improvement Proposals, March 4, 2019. https://eips.ethereum.org/EIPS/eip-1822.
  5. @luu-alex, @gabrocheleau, @wackerow, @minimalsm, @kuzdogan, and @jmcook1186. “Recursive-Length Prefix (RLP) Serialization.” ethereum.org. Accessed April 17, 2023. https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/.
  6. (@vbuterin), Vitalik Buterin. “EIP-1014: Skinny Create2.” Ethereum Improvement Proposals, April 20, 2018. https://eips.ethereum.org/EIPS/eip-1014.
  7. “Wintermute.” rekt. Accessed April 17, 2023. https://rekt.news/wintermute-rekt/.
  8. “Proxies Deep Dive.” yAcademy Proxies Research. Accessed April 17, 2023. https://proxies.yacademy.dev/pages/proxies-list/.
  9. “Security Guide to Proxies.” yAcademy Proxies Research. Accessed April 17, 2023. https://proxies.yacademy.dev/pages/security-guide.
  10. Croubois, Hadrien. “Workshop Recap: Cheap Contract Deployment Through Clones.” OpenZeppelin, March 3, 2021. https://blog.openzeppelin.com/workshop-recap-cheap-contract-deployment-through-clones/.
  11. Giordano, Francisco. “Deploying More Efficient Upgradeable Contracts.” OpenZeppelin, June 17, 2021. https://blog.openzeppelin.com/workshop-recap-deploying-more-efficient-upgradeable-contracts/.

Artigo original publicado por scourgedev.eth. Traduzido por Paulinho Giovannini.

Top comments (0)