Os contratos de proxy são uma ferramenta importante para desenvolvedores de contratos inteligentes. Hoje, existem muitos modos de proxy e regras de uso correspondentes no sistema de contrato. Descrevemos anteriormente as melhores práticas de segurança de contratos de proxy atualizáveis.
Neste artigo, apresentaremos outro modelo de proxy popular na comunidade de desenvolvedores, o modelo de proxy Diamond.
Os contratos de proxy de diamante, também conhecidos como "diamantes", são um padrão de design para contratos inteligentes Ethereum introduzidos pela Ethereum Improvement Proposal (EIP) 2535.
O modo Diamante permite que um contrato tenha funcionalidade ilimitada, dividindo sua funcionalidade em contratos menores (também chamados de "aspectos"). Diamond atua como um proxy, roteando chamadas de função para o aspecto apropriado.
O design do modelo de diamante pode resolver o problema da limitação do tamanho máximo do contrato da rede Ethereum. Ao dividir um grande contrato em aspectos menores, o padrão de diamante permite que os desenvolvedores criem contratos inteligentes mais complexos e ricos em recursos sem serem afetados por restrições de tamanho.
A Diamond Brokerage oferece uma enorme flexibilidade em comparação com os contratos atuais atualizáveis. Eles permitem que as partes do contrato sejam atualizadas, adicionando, substituindo ou removendo partes selecionadas de funções sem tocar em outras partes.
Este artigo fornece uma visão geral do EIP-2535, incluindo uma comparação com o modo de proxy transparente amplamente usado e o modo de proxy UUPS e suas considerações de segurança para a comunidade de desenvolvedores.
No contexto da EIP-2535, um "diamante" é um contrato proxy cuja implementação funcional é fornecida por diferentes contratos lógicos, chamados de "aspectos".
Imagine que um diamante real tenha lados diferentes, chamados de facetas, e os contratos de diamante Ethereum correspondentes também tenham facetas diferentes. Cada contrato de função de empréstimo de diamante é um lado ou faceta diferente.
O padrão de diamante usa uma analogia para estender os recursos do "corte de diamante" para adicionar, substituir ou excluir facetas e recursos.
Além disso, o Diamond Standard fornece um recurso chamado "Diamond Loupe" que retorna informações sobre as facetas e a presença do diamante.
Em comparação com o modelo de procuração tradicional, o “diamante” equivale ao contrato de procuração, e os diferentes “aspectos” correspondem à realização do contrato. Diferentes aspectos de um agente diamante podem compartilhar funções internas, bibliotecas e variáveis de estado. Os principais componentes de um diamante são os seguintes:
Um contrato central que atua como um proxy, roteando chamadas de função para o aspecto apropriado. Ele contém um mapeamento de seletores de função para endereços de "aspecto".
Um único contrato que implementa uma função específica. Cada faceta contém um conjunto de funções que podem ser chamadas pelo diamante.
é um conjunto de funções padrão definidas no EIP-2535 que fornecem informações sobre seletores de função e faceta usados em diamantes. Diamond Loupe permite que desenvolvedores e usuários inspecionem e entendam a estrutura dos diamantes.
Funções para adicionar, substituir ou remover facetas em um diamante e seus seletores de recursos correspondentes. Somente endereços autorizados (por exemplo, o proprietário do diamante ou um contrato de assinatura múltipla) podem executar o corte de diamante.
Semelhante aos agentes tradicionais, quando uma chamada de função é feita no agente diamante, a função fallback do agente (função fallback) é acionada. A principal diferença em relação ao proxy diamond é que na função fallback existe um mapeamento selectorToFacet, que armazena e determina qual endereço lógico do contrato possui a implementação da função chamada. Em seguida, ele usa um delegatecall para executar a função, assim como um proxy tradicional.
Todos os proxies usam a função fallback() para delegar chamadas de função para endereços externos. Abaixo está a implementação do proxy Diamond e a implementação do proxy tradicional.
Vale a pena notar que seus blocos de código de montagem são muito semelhantes, então a única diferença é o endereço de aspecto na chamada de delegado de proxy Diamond e o endereço de impl na chamada de delegado de proxy tradicional.
A principal diferença é que no proxy Diamond, o endereço do aspecto é determinado pelo hashmap do msg.sig do chamador (seletor de função) para o endereço do aspecto, enquanto no proxy tradicional, o endereço impl não depende de a entrada do chamador.
Função de fallback do agente Diamond
Função tradicional de fallback de proxy
O mapeamento SelectorToFacet determina qual contrato contém a implementação de cada seletor de função. Os trabalhadores do projeto geralmente precisam adicionar, substituir ou remover esse mapeamento de contrato de seletor de função para implementação. EIP-2535 estipula: Para atingir este propósito, deve haver uma função diamondCut(). Abaixo está um exemplo de interface.
Cada estrutura FacetCut contém um endereço de faceta e uma matriz seletora de recursos de quatro bytes a ser atualizada no contrato de proxy de diamante. FaceCutAction permite adicionar, substituir e remover seletores de recursos. A implementação da função diamondCut() deve incluir controle de acesso adequado, prevenir colisões de slots, recuperar em caso de falha, etc.
Para consultar quais funções e facetas possui um agente diamante, utilizamos a "lupa diamante". "Diamond Loupe" é um aspecto especial que implementa a seguinte interface definida no EIP-2535:
A função facets() deve retornar os endereços de todas as facetas e seus seletores de função de quatro bytes. A função facetFunctionSelectors() deve retornar todos os seletores de função suportados por um determinado aspecto. A função facetAddresses() deve retornar todos os endereços de facetas usados por um diamante.
A função facetAddress() deve retornar um aspecto que suporte o seletor fornecido ou address(0) se não for encontrado. Observe que não deve haver mais de um endereço de aspecto com o mesmo seletor de recursos.
Dado que os proxies Diamond delegam diferentes chamadas de função para diferentes contratos de implementação, é fundamental gerenciar os slots de armazenamento adequadamente para evitar conflitos. O EIP-2535 menciona vários métodos de gerenciamento de slots de armazenamento.
Este aspecto pode declarar variáveis de estado na estrutura. Este aspecto pode usar qualquer número de estruturas, cada uma com um local de armazenamento diferente. Cada estrutura tem um local específico no armazenamento do contrato. Aspectos podem declarar suas próprias variáveis de estado, mas não podem entrar em conflito com os locais de armazenamento de variáveis de estado declarados por outros aspectos. Uma biblioteca de amostra e um contrato de armazenamento de diamantes são fornecidos no EIP-2535, conforme mostrado na figura a seguir:
O armazenamento de aplicativos é uma versão mais especializada do armazenamento de diamantes. Esse padrão é usado para compartilhar variáveis de estado de aspectos de maneira mais conveniente e fácil. Uma estrutura de loja de aplicativos é definida para conter qualquer número e tipo de variáveis de estado exigidas por um aplicativo. Um aspecto sempre declara a estrutura AppStorage como a primeira e única variável de estado, na posição 0 do slot de armazenamento. Diferentes aspectos podem acessar variáveis dessa estrutura.
Existem também outras estratégias de gerenciamento de slots de armazenamento, incluindo um híbrido de Diamond Storage e AppStorage. Por exemplo, algumas estruturas são compartilhadas entre diferentes aspectos e algumas são específicas para um aspecto específico. Em todos os casos, é muito importante evitar colisões acidentais de slots.
Comparação com Transparent Proxy e UUPS Proxy
Os dois principais modos de proxy usados atualmente pela comunidade de desenvolvedores Web3 são o modo de proxy transparente e o modo de proxy UUPS. Nesta seção, comparamos brevemente o modo proxy Diamond com os modos proxy transparente e proxy UUPS.
1.EPI-2535:
2.EPI-1967:
3. Implementação de referência de proxy Diamond:
4.Implementação do OpenZeppelin:
Proxy e soluções escaláveis são sistemas mais complexos, e o OpenZeppelin fornece bases de código e documentação abrangente para proxies escaláveis UUPS, Transparent e Beacon. No entanto, para o modo de proxy de diamante, embora o OpenZeppelin afirmasse seus benefícios, eles ainda decidiram não incluir a implementação de diamante EIP-2535 em sua biblioteca.
Portanto, os desenvolvedores que usam bibliotecas de terceiros existentes ou implementam essa solução por conta própria devem implementá-la com extremo cuidado. Aqui, compilamos uma lista de verificação das melhores práticas de segurança para a comunidade de desenvolvedores considerar.
Ao dividir a lógica do contrato em módulos menores e mais gerenciáveis, os desenvolvedores podem testar e auditar seu código com mais facilidade.
Além disso, essa abordagem permite que os desenvolvedores se concentrem em aspectos específicos da criação e manutenção de contratos, em vez de gerenciar uma base de código monolítica e complexa. O resultado final é uma base de código mais flexível e modular que pode ser facilmente atualizada e modificada sem afetar outras partes do contrato.
Fonte: Aavegotchi Github
Quando o contrato de proxy de diamante é implantado, ele deve adicionar o endereço do contrato DiamondCutFacet ao contrato de proxy de diamante e implementar a função diamondCut(). A função diamondCut() é usada para adicionar, excluir ou substituir facetas e funções, sem DiamondCutFacet e diamondCut(), o agente de diamante não pode funcionar corretamente.
Fonte: Diamond-3-hardhat de Mugen
Ao adicionar uma nova variável de estado a uma estrutura de armazenamento em um contrato inteligente, ela deve ser adicionada no final da estrutura. Adicionar uma nova variável de estado no início ou no meio de uma estrutura fará com que a nova variável de estado sobrescreva os dados da variável de estado existente, e qualquer variável de estado após a nova variável de estado pode se referir ao local de memória errado.
O padrão AppStorage requer que uma e apenas uma estrutura seja declarada para o proxy Diamond e que essa estrutura seja compartilhada por todos os aspectos. Se forem necessárias várias estruturas, o padrão DiamondStorage deve ser usado.
Não coloque uma struct diretamente dentro de outra struct, a menos que tenha certeza de que não pretende adicionar mais variáveis de estado à struct interna. Não é possível adicionar novas variáveis de estado a estruturas internas em uma atualização sem sobrescrever os slots de armazenamento de variáveis declarados após a estrutura.
A solução é adicionar novas variáveis de estado ao struct mapeado em memória em vez de colocar o "struct" diretamente no "struct". Os slots de armazenamento variável em um mapa são calculados de forma diferente e não são contíguos no armazenamento.
O tamanho da matriz será afetado pelo tamanho da estrutura. Quando uma nova variável de estado é adicionada a uma estrutura, ela altera o tamanho e o layout dessa estrutura.
Isso pode causar problemas se a estrutura for usada como um elemento em uma matriz. Se o tamanho e o layout da estrutura forem alterados, o tamanho e o layout da matriz também serão alterados, o que pode causar problemas com a indexação ou outras operações que dependem de tamanho e layout consistentes da estrutura.
Semelhante a outros padrões de proxy, cada variável deve ter um slot de armazenamento exclusivo. Caso contrário, duas estruturas diferentes no mesmo local substituiriam uma à outra.
A função initialize() geralmente é usada para definir variáveis importantes, como endereços de funções privilegiadas. Se não for inicializado quando o contrato for implantado, um ator mal-intencionado poderá chamar e controlar o contrato.
Recomenda-se adicionar controle de acesso apropriado à função de inicialização/configuração ou garantir que a função seja chamada quando o contrato for implantado e não possa ser chamada novamente.
Se algum aspecto do contrato for capaz de chamar a função selfdestruct(), ele poderá destruir todo o contrato, resultando em perda de fundos ou dados. Isso é extremamente perigoso no modo de proxy Diamond, pois vários aspectos podem acessar o armazenamento e os dados do contrato de proxy.
Atualmente, vemos cada vez mais projetos adotando o modelo de proxy de diamante em seus contratos inteligentes. Oferece flexibilidade e outras vantagens em relação aos proxies tradicionais.
No entanto, a flexibilidade extra também pode significar uma superfície de ataque mais ampla para os atacantes. Esperamos que este artigo ajude a comunidade de desenvolvedores a entender a mecânica do modelo de proxy Diamond e suas considerações de segurança.
Ao mesmo tempo, a equipe do projeto deve realizar testes rigorosos e auditorias de terceiros para reduzir o risco de vulnerabilidades relacionadas à implementação de contratos de agenciamento de diamantes.
Ver original
O conteúdo é apenas para referência, não uma solicitação ou oferta. Nenhum aconselhamento fiscal, de investimento ou jurídico é fornecido. Consulte a isenção de responsabilidade para obter mais informações sobre riscos.
Melhores práticas de segurança para contratos de agências de diamantes
Os contratos de proxy são uma ferramenta importante para desenvolvedores de contratos inteligentes. Hoje, existem muitos modos de proxy e regras de uso correspondentes no sistema de contrato. Descrevemos anteriormente as melhores práticas de segurança de contratos de proxy atualizáveis.
Neste artigo, apresentaremos outro modelo de proxy popular na comunidade de desenvolvedores, o modelo de proxy Diamond.
Os contratos de proxy de diamante, também conhecidos como "diamantes", são um padrão de design para contratos inteligentes Ethereum introduzidos pela Ethereum Improvement Proposal (EIP) 2535.
O modo Diamante permite que um contrato tenha funcionalidade ilimitada, dividindo sua funcionalidade em contratos menores (também chamados de "aspectos"). Diamond atua como um proxy, roteando chamadas de função para o aspecto apropriado.
O design do modelo de diamante pode resolver o problema da limitação do tamanho máximo do contrato da rede Ethereum. Ao dividir um grande contrato em aspectos menores, o padrão de diamante permite que os desenvolvedores criem contratos inteligentes mais complexos e ricos em recursos sem serem afetados por restrições de tamanho.
A Diamond Brokerage oferece uma enorme flexibilidade em comparação com os contratos atuais atualizáveis. Eles permitem que as partes do contrato sejam atualizadas, adicionando, substituindo ou removendo partes selecionadas de funções sem tocar em outras partes.
Este artigo fornece uma visão geral do EIP-2535, incluindo uma comparação com o modo de proxy transparente amplamente usado e o modo de proxy UUPS e suas considerações de segurança para a comunidade de desenvolvedores.
No contexto da EIP-2535, um "diamante" é um contrato proxy cuja implementação funcional é fornecida por diferentes contratos lógicos, chamados de "aspectos".
Imagine que um diamante real tenha lados diferentes, chamados de facetas, e os contratos de diamante Ethereum correspondentes também tenham facetas diferentes. Cada contrato de função de empréstimo de diamante é um lado ou faceta diferente.
O padrão de diamante usa uma analogia para estender os recursos do "corte de diamante" para adicionar, substituir ou excluir facetas e recursos.
Além disso, o Diamond Standard fornece um recurso chamado "Diamond Loupe" que retorna informações sobre as facetas e a presença do diamante.
Em comparação com o modelo de procuração tradicional, o “diamante” equivale ao contrato de procuração, e os diferentes “aspectos” correspondem à realização do contrato. Diferentes aspectos de um agente diamante podem compartilhar funções internas, bibliotecas e variáveis de estado. Os principais componentes de um diamante são os seguintes:
Um contrato central que atua como um proxy, roteando chamadas de função para o aspecto apropriado. Ele contém um mapeamento de seletores de função para endereços de "aspecto".
Um único contrato que implementa uma função específica. Cada faceta contém um conjunto de funções que podem ser chamadas pelo diamante.
é um conjunto de funções padrão definidas no EIP-2535 que fornecem informações sobre seletores de função e faceta usados em diamantes. Diamond Loupe permite que desenvolvedores e usuários inspecionem e entendam a estrutura dos diamantes.
Funções para adicionar, substituir ou remover facetas em um diamante e seus seletores de recursos correspondentes. Somente endereços autorizados (por exemplo, o proprietário do diamante ou um contrato de assinatura múltipla) podem executar o corte de diamante.
Semelhante aos agentes tradicionais, quando uma chamada de função é feita no agente diamante, a função fallback do agente (função fallback) é acionada. A principal diferença em relação ao proxy diamond é que na função fallback existe um mapeamento selectorToFacet, que armazena e determina qual endereço lógico do contrato possui a implementação da função chamada. Em seguida, ele usa um delegatecall para executar a função, assim como um proxy tradicional.
Todos os proxies usam a função fallback() para delegar chamadas de função para endereços externos. Abaixo está a implementação do proxy Diamond e a implementação do proxy tradicional.
Vale a pena notar que seus blocos de código de montagem são muito semelhantes, então a única diferença é o endereço de aspecto na chamada de delegado de proxy Diamond e o endereço de impl na chamada de delegado de proxy tradicional.
A principal diferença é que no proxy Diamond, o endereço do aspecto é determinado pelo hashmap do msg.sig do chamador (seletor de função) para o endereço do aspecto, enquanto no proxy tradicional, o endereço impl não depende de a entrada do chamador.
Função de fallback do agente Diamond
Função tradicional de fallback de proxy
O mapeamento SelectorToFacet determina qual contrato contém a implementação de cada seletor de função. Os trabalhadores do projeto geralmente precisam adicionar, substituir ou remover esse mapeamento de contrato de seletor de função para implementação. EIP-2535 estipula: Para atingir este propósito, deve haver uma função diamondCut(). Abaixo está um exemplo de interface.
Cada estrutura FacetCut contém um endereço de faceta e uma matriz seletora de recursos de quatro bytes a ser atualizada no contrato de proxy de diamante. FaceCutAction permite adicionar, substituir e remover seletores de recursos. A implementação da função diamondCut() deve incluir controle de acesso adequado, prevenir colisões de slots, recuperar em caso de falha, etc.
Para consultar quais funções e facetas possui um agente diamante, utilizamos a "lupa diamante". "Diamond Loupe" é um aspecto especial que implementa a seguinte interface definida no EIP-2535:
A função facets() deve retornar os endereços de todas as facetas e seus seletores de função de quatro bytes. A função facetFunctionSelectors() deve retornar todos os seletores de função suportados por um determinado aspecto. A função facetAddresses() deve retornar todos os endereços de facetas usados por um diamante.
A função facetAddress() deve retornar um aspecto que suporte o seletor fornecido ou address(0) se não for encontrado. Observe que não deve haver mais de um endereço de aspecto com o mesmo seletor de recursos.
Dado que os proxies Diamond delegam diferentes chamadas de função para diferentes contratos de implementação, é fundamental gerenciar os slots de armazenamento adequadamente para evitar conflitos. O EIP-2535 menciona vários métodos de gerenciamento de slots de armazenamento.
Este aspecto pode declarar variáveis de estado na estrutura. Este aspecto pode usar qualquer número de estruturas, cada uma com um local de armazenamento diferente. Cada estrutura tem um local específico no armazenamento do contrato. Aspectos podem declarar suas próprias variáveis de estado, mas não podem entrar em conflito com os locais de armazenamento de variáveis de estado declarados por outros aspectos. Uma biblioteca de amostra e um contrato de armazenamento de diamantes são fornecidos no EIP-2535, conforme mostrado na figura a seguir:
O armazenamento de aplicativos é uma versão mais especializada do armazenamento de diamantes. Esse padrão é usado para compartilhar variáveis de estado de aspectos de maneira mais conveniente e fácil. Uma estrutura de loja de aplicativos é definida para conter qualquer número e tipo de variáveis de estado exigidas por um aplicativo. Um aspecto sempre declara a estrutura AppStorage como a primeira e única variável de estado, na posição 0 do slot de armazenamento. Diferentes aspectos podem acessar variáveis dessa estrutura.
Existem também outras estratégias de gerenciamento de slots de armazenamento, incluindo um híbrido de Diamond Storage e AppStorage. Por exemplo, algumas estruturas são compartilhadas entre diferentes aspectos e algumas são específicas para um aspecto específico. Em todos os casos, é muito importante evitar colisões acidentais de slots.
Comparação com Transparent Proxy e UUPS Proxy
Os dois principais modos de proxy usados atualmente pela comunidade de desenvolvedores Web3 são o modo de proxy transparente e o modo de proxy UUPS. Nesta seção, comparamos brevemente o modo proxy Diamond com os modos proxy transparente e proxy UUPS.
1.EPI-2535:
2.EPI-1967:
3. Implementação de referência de proxy Diamond:
4.Implementação do OpenZeppelin:
Proxy e soluções escaláveis são sistemas mais complexos, e o OpenZeppelin fornece bases de código e documentação abrangente para proxies escaláveis UUPS, Transparent e Beacon. No entanto, para o modo de proxy de diamante, embora o OpenZeppelin afirmasse seus benefícios, eles ainda decidiram não incluir a implementação de diamante EIP-2535 em sua biblioteca.
Portanto, os desenvolvedores que usam bibliotecas de terceiros existentes ou implementam essa solução por conta própria devem implementá-la com extremo cuidado. Aqui, compilamos uma lista de verificação das melhores práticas de segurança para a comunidade de desenvolvedores considerar.
Ao dividir a lógica do contrato em módulos menores e mais gerenciáveis, os desenvolvedores podem testar e auditar seu código com mais facilidade.
Além disso, essa abordagem permite que os desenvolvedores se concentrem em aspectos específicos da criação e manutenção de contratos, em vez de gerenciar uma base de código monolítica e complexa. O resultado final é uma base de código mais flexível e modular que pode ser facilmente atualizada e modificada sem afetar outras partes do contrato.
Fonte: Aavegotchi Github
Quando o contrato de proxy de diamante é implantado, ele deve adicionar o endereço do contrato DiamondCutFacet ao contrato de proxy de diamante e implementar a função diamondCut(). A função diamondCut() é usada para adicionar, excluir ou substituir facetas e funções, sem DiamondCutFacet e diamondCut(), o agente de diamante não pode funcionar corretamente.
Fonte: Diamond-3-hardhat de Mugen
Ao adicionar uma nova variável de estado a uma estrutura de armazenamento em um contrato inteligente, ela deve ser adicionada no final da estrutura. Adicionar uma nova variável de estado no início ou no meio de uma estrutura fará com que a nova variável de estado sobrescreva os dados da variável de estado existente, e qualquer variável de estado após a nova variável de estado pode se referir ao local de memória errado.
O padrão AppStorage requer que uma e apenas uma estrutura seja declarada para o proxy Diamond e que essa estrutura seja compartilhada por todos os aspectos. Se forem necessárias várias estruturas, o padrão DiamondStorage deve ser usado.
Não coloque uma struct diretamente dentro de outra struct, a menos que tenha certeza de que não pretende adicionar mais variáveis de estado à struct interna. Não é possível adicionar novas variáveis de estado a estruturas internas em uma atualização sem sobrescrever os slots de armazenamento de variáveis declarados após a estrutura.
A solução é adicionar novas variáveis de estado ao struct mapeado em memória em vez de colocar o "struct" diretamente no "struct". Os slots de armazenamento variável em um mapa são calculados de forma diferente e não são contíguos no armazenamento.
O tamanho da matriz será afetado pelo tamanho da estrutura. Quando uma nova variável de estado é adicionada a uma estrutura, ela altera o tamanho e o layout dessa estrutura.
Isso pode causar problemas se a estrutura for usada como um elemento em uma matriz. Se o tamanho e o layout da estrutura forem alterados, o tamanho e o layout da matriz também serão alterados, o que pode causar problemas com a indexação ou outras operações que dependem de tamanho e layout consistentes da estrutura.
Semelhante a outros padrões de proxy, cada variável deve ter um slot de armazenamento exclusivo. Caso contrário, duas estruturas diferentes no mesmo local substituiriam uma à outra.
A função initialize() geralmente é usada para definir variáveis importantes, como endereços de funções privilegiadas. Se não for inicializado quando o contrato for implantado, um ator mal-intencionado poderá chamar e controlar o contrato.
Recomenda-se adicionar controle de acesso apropriado à função de inicialização/configuração ou garantir que a função seja chamada quando o contrato for implantado e não possa ser chamada novamente.
Se algum aspecto do contrato for capaz de chamar a função selfdestruct(), ele poderá destruir todo o contrato, resultando em perda de fundos ou dados. Isso é extremamente perigoso no modo de proxy Diamond, pois vários aspectos podem acessar o armazenamento e os dados do contrato de proxy.
Atualmente, vemos cada vez mais projetos adotando o modelo de proxy de diamante em seus contratos inteligentes. Oferece flexibilidade e outras vantagens em relação aos proxies tradicionais.
No entanto, a flexibilidade extra também pode significar uma superfície de ataque mais ampla para os atacantes. Esperamos que este artigo ajude a comunidade de desenvolvedores a entender a mecânica do modelo de proxy Diamond e suas considerações de segurança.
Ao mesmo tempo, a equipe do projeto deve realizar testes rigorosos e auditorias de terceiros para reduzir o risco de vulnerabilidades relacionadas à implementação de contratos de agenciamento de diamantes.