WEB3DEV

Cover image for Desvendando as Funções call e delegatecall do Solidity: Compreendendo as Diferenças e os Riscos de Segurança
Paulo Gio
Paulo Gio

Posted on

Desvendando as Funções call e delegatecall do Solidity: Compreendendo as Diferenças e os Riscos de Segurança

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*H_fEcS59NsIiI7p_Tn5OiA.jpeg

Sumário

  • Introdução
  • Definindo call e delegatecall
  • Exemplos usando call e delegatecall
  • Diferentes maneiras de usar call e delegatecall
  • Valores de retorno em call e delegatecall
  • Casos de uso de call vs delegatecall
  • Considerações de segurança ao usar call ou delegatecall
  • Algumas diferenças e semelhanças
  • Conclusões

Introdução

Nesta postagem do blog, quero descrever os casos de uso de call e delegatecall, bem como suas diferenças, como os contratos inteligentes podem usá-los e, claro, considerações de segurança para desenvolvedores e auditores de contratos inteligentes quando encontrarem esses termos no campo.

Se você quiser se conectar comigo, siga-me no Twitter em @s3rgiomazari3go, minhas DMs estão abertas para ajudar.

Definindo call e delegatecall

Antes de começarmos, vamos definir cada uma das funções:

call: A função call em Solidity é uma função de baixo nível que permite a um contrato (chamador) invocar funções de outro contrato (chamado). Ela opera no contexto do chamador, executando o código no contrato chamado, enquanto preserva o armazenamento e o contexto do chamador. Na invocação, a função call retorna um valor booleano indicando o sucesso ou a falha da chamada da função, juntamente com um fluxo de bytes contendo qualquer dado de retorno do contrato chamado (mais sobre isso mais tarde).

delegatecall: A função delegatecall também é uma função de baixo nível em Solidity que permite a um contrato (chamador) invocar funções de outro contrato (chamado) com algumas distinções notáveis. Diferente de call, delegatecall executa o código do contrato chamado no contexto do chamador. Isso significa que ele herda o armazenamento e o contexto do chamador, potencialmente modificando seu estado. Similar à função call, delegatecall retorna um valor booleano indicando o sucesso ou a falha da chamada da função.

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*7DtOCYzTza_tHYl0SLc-Mg.png

Vamos considerar um cenário em que o contrato A atua como chamador e o contrato B como chamado. O contrato A tem funções que usam call e delegatecall para definir um valor em uma variável do contrato B. Com call, o contrato A pode invocar uma função no contrato B, passando o valor a ser definido como um argumento. A função no contrato B é executada em seu próprio contexto, e o valor é armazenado em seu próprio armazenamento.

No entanto, em delegatecall, a função do contrato B é executada dentro do contexto do contrato A. Isso significa que qualquer modificação feita no armazenamento dentro da função chamada por meio de delegatecall afetará o armazenamento do contrato A, não do contrato B, e o contrato chamado pode ler e escrever no armazenamento do contrato chamador.

A escolha entre call e delegatecall depende se o contrato chamado deve operar em seu próprio contexto (call) ou modificar o armazenamento do chamador (delegatecall).

Exemplos usando call e delegatecall

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*qP8guR5-i5rEMsjGStqU6w.png

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*bxOpOtF4knimVpzV8naHbg.png

No Contrato B, definimos um contrato simples com uma variável de valor (value) e uma função setValue, que atualiza o valor.

No Contrato A, temos duas funções: callSetValue e delegateCallSetValue. Essas funções aceitam um endereço de outro contrato e um novo valor.

  • callSetValue usa a função call para invocar a função setValue do contrato especificado. Ela opera no contexto do Contrato A e atualiza o armazenamento do contrato chamado. Quaisquer mudanças de estado ocorrem no contrato chamado, e o armazenamento do contrato chamador permanece inalterado.
  • delegateCallSetValue usa a função delegatecall para invocar a função setValue do contrato especificado. Ela opera no contexto do contrato chamador (Contrato A) e atualiza o armazenamento do próprio Contrato A. O armazenamento e o contexto do contrato chamado são herdados pelo contrato chamador, potencialmente afetando seu estado.

Ao implantar o Contrato A e o Contrato B, e chamar as respectivas funções, você pode observar as diferenças em como a variável value é atualizada e qual armazenamento do contrato é afetado.

Por que é necessário que o Contrato A replique a ordem das variáveis no Contrato B ao usar delegatecall, enquanto não há problema em alterar a ordem das variáveis em ambos os contratos ao usar uma função call regular?

Em delegatecall, é necessário que o Contrato A replique a ordem das variáveis no Contrato B porque o código do contrato chamado é executado dentro do contexto do contrato chamador. Isso significa que o Contrato B opera usando o layout de armazenamento do Contrato A, e quaisquer modificações no armazenamento do Contrato A devem aderir à sua ordem de variáveis original. Incompatibilidades na ordem das variáveis entre os dois contratos podem levar a uma interpretação incorreta dos dados e comportamento inesperado.

Por outro lado, em uma função call regular, o código é executado dentro do contexto do próprio contrato chamado e cada contrato tem seu próprio layout de armazenamento e ordem de variáveis.

Em outras palavras, em delegatecall, o contrato chamado opera dentro do contexto do contrato chamador, exigindo a replicação da ordem das variáveis para garantir o manuseio adequado dos dados, mas em uma função call regular, cada contrato opera dentro de seu próprio contexto, permitindo flexibilidade na ordem das variáveis desde que permaneça consistente dentro de cada contrato.

Diferentes maneiras de usar call e delegatecall

Existem diferentes maneiras de usar call e delegatecall em um contrato. Nesta seção, compartilharei o que, na minha opinião, são as mais comuns e necessárias para entender.

Spoiler: Quando você usa delegatecall, não é possível enviar ETH diretamente com a invocação da função.

Chamando uma função de outro contrato

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*nWnqq87phRNJmVU0gYu70A.png

Chamando uma função de outro contrato, enviando valor (ETH)

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*tywwOREVVW_Hg8SFlRFCjw.png

Chamando uma função com um argumento

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*JkbN_3c4VVH9K6LBuo3ZSQ.png

Chamando uma função com dois argumentos do mesmo tipo de dados

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*NzIXpUAqEKnWHHXrbw2m9w.png

Chamando uma função com dois argumentos de diferentes tipos de dados

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*6nVFqMX2DF42y20SxWHMhg.png

Valores de retorno em call e delegatecall

Ao usar os métodos call e delegatecall em Solidity, o valor de retorno indica o sucesso ou a falha da chamada da função. O valor de retorno é uma tupla composta por dois elementos: um valor booleano e os dados de retorno.

O valor booleano indica se a chamada da função foi bem-sucedida ou não. Será true se a chamada foi bem-sucedida e false se falhar. Uma chamada pode falhar por vários motivos, como uma exceção sendo lançada dentro da função chamada, uma condição de falta de gás, ou se a função de fallback do contrato chamado não existir ou reverter.

Os dados de retorno são do tipo bytes memory e contêm quaisquer dados retornados pela função chamada. Se a função que está sendo chamada tem um valor de retorno, este valor será codificado e armazenado na variável returnData. Você pode então decodificar e interpretar os dados de retorno de acordo com o tipo de retorno esperado da função chamada.

Aqui está um exemplo de como você pode lidar com o valor de retorno de call ou delegatecall:

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*xrFxo5th07WNOqp5ozBpjw.png

No exemplo acima, capturamos o valor de retorno do método call na tupla (bool success, bytes memory returnData) e então verificamos o booleano success para garantir que a chamada da função foi bem-sucedida. Se falhar, podemos lidar com a falha de maneira apropriada, como reverter a transação ou tomar outras ações necessárias.

Se returnData tiver um comprimento maior que zero, significa que a função chamada retornou alguns dados. Podemos decodificar e processar esses dados com base no tipo de retorno esperado. A lógica específica de decodificação e processamento dependerá do tipo de retorno da função chamada.

Outro exemplo

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*FGkpPLggbvgvX0GX5d6Fwg.png

Neste código, após armazenar os dados de retorno em returnData, a função abi.decode é usada para decodificar os dados de retorno. Especificamos os tipos esperados (uint256, string) no segundo parâmetro de abi.decode para decodificar os dados de retorno de acordo.

Ao incluir abi.decode, os dados de retorno são devidamente decodificados e atribuídos às variáveis processedNumber e processedString. Isso nos permite retornar os resultados processados, como uma tupla de uint256 e string.

É importante notar que, ao usar delegatecall, os dados de retorno refletirão o valor de retorno da função que está sendo chamada, mas o contexto e o armazenamento serão do contrato chamador, e não do contrato chamado.

Call vs delegatecall: Casos de uso

Agora que sabemos como call e delegatecall podem ser usados, nesta seção quero apontar alguns dos casos de uso mais comuns dessas funções.

call: A função call é comumente usada quando se deseja invocar funções de contratos ou endereços externos. Ela opera no contexto do contrato chamador, preservando o armazenamento e o contexto deste último. Isso a torna útil para cenários em que você precisa interagir com contratos externos, mas quer manter o controle sobre o estado do seu próprio contrato. Além disso, call pode ser usada para enviar ETH junto com a chamada da função, tornando-a adequada para realizar pagamentos ou transferir valor durante as invocações de função.

Alguns casos de uso comuns para call incluem:

  • Acessar funções de contratos externos: Você pode usar call para invocar funções específicas de outros contratos e recuperar seus valores de retorno.
  • Oráculos e recuperação de dados: Fontes de dados externas podem ser acessadas usando call para buscar informações de APIs ou sistemas externos.
  • Interagir com interfaces de contrato: Ao trabalhar com contratos que implementam interfaces específicas, call permite que você invoque funções de interface e recupere dados ou acione ações.

delegatecall: Por outro lado, delegatecall é um mecanismo de invocação de função poderoso que executa o código do contrato chamado no contexto do contrato chamador. Isso significa que herda o armazenamento e o contexto do chamador, permitindo que modifique diretamente o estado do contrato chamador. Isso pode ser vantajoso quando você deseja integrar bibliotecas reutilizáveis ou delegar certas funcionalidades a contratos externos enquanto mantém o mesmo armazenamento no contrato chamador.

Aqui estão alguns casos de uso comuns para delegatecall:

  • Integração de bibliotecas: Ao usar delegatecall, você pode incorporar bibliotecas externas ao seu contrato, permitindo a reutilização de código e redução de custos de implantação. O código da biblioteca será executado no contexto do seu contrato, permitindo que acesse e modifique o armazenamento do seu contrato.
  • Atualização de contratos: delegatecall pode ser usado para atualizações de contrato, onde uma nova lógica de contrato é implantada e então chamada por meio de delegatecall a partir do contrato existente. Desta forma, a nova lógica opera dentro do contexto do contrato existente, preservando o armazenamento e o estado.
  • Design de contrato modular: delegatecall facilita o design de contrato modular, permitindo que contratos separados sejam invocados e operados dentro do contexto de um único contrato, possibilitando funcionalidade modular e reduzindo a complexidade do contrato.

Considerações de segurança ao usar call ou delegatecall

Ao utilizar as funções call e delegatecall no Solidity, é vital compreender e abordar as considerações de segurança associadas. Ambos os mecanismos de invocação de função podem introduzir riscos potenciais se não forem usados ​​com cuidado. Nesta seção, exploraremos algumas considerações de segurança essenciais para ter em mente ao empregar call e delegatecall em seus contratos inteligentes. Você pode encontrar os detalhes na seção de referências.

Interação com Contratos Não Confiáveis: Ao usar call ou delegatecall para interagir com contratos externos, certifique-se de validar e sanear cuidadosamente qualquer entrada ou dado recebido desses contratos. Contratos externos podem conter código malicioso ou vulnerabilidades que podem ser exploradas. Valide entradas, realize verificações de limite adequadas e use práticas seguras de manipulação de dados para prevenir possíveis violações de segurança.

Proteção de Armazenamento: delegatecall permite que contratos externos modifiquem o armazenamento do contrato chamador diretamente. Embora esse recurso possa ser poderoso para design modular e atualizações de contrato, também introduz riscos. Tenha cuidado ao usar delegatecall e certifique-se de proteger variáveis ​​de armazenamento críticas ou sensíveis de modificações não intencionais. Implemente mecanismos de controle de acesso apropriados e verificações de validação para prevenir modificações não autorizadas no armazenamento do contrato chamador.

Limite de Gás e Ataques Out-of-Gas: Ambas as operações call e delegatecall consomem gás, e é crucial considerar o potencial uso de gás ao fazer invocações de função. Operações grandes ou complexas realizadas via call ou delegatecall podem consumir gás excessivo, levando a cenários “out-of-gas” (sem gás). Isso pode resultar em transações falhadas ou comportamento inesperado. Analise cuidadosamente os requisitos de gás, implemente limites de gás e realize testes abrangentes para mitigar os riscos relacionados ao gás.

Ataques de Reentrância: Considere cuidadosamente o potencial para ataques de reentrância ao usar call ou delegatecall. A reentrância ocorre quando um contrato externo maliciosamente invoca de volta ao contrato chamador antes que a invocação inicial seja concluída. Isso pode levar a um comportamento inesperado e pode resultar em perdas financeiras ou acesso não autorizado. Implemente padrões apropriados de mutex ou guarda de reentrância para prevenir tais ataques e garantir uma segurança robusta.

Algumas Diferenças e Semelhanças

Call Delegatecall
Propósito Invoca uma função em outro contrato Invoca uma função em outro contrato, preservando o armazenamento e contexto do chamador
Contexto de Execução Usa o contexto de execução do chamador Usa o contexto do contrato onde delegatecall é executado
Execução de Código Executa a função chamada no contexto do contrato chamado Executa a função chamada no contexto do contrato chamador
Alterações de Estado Atualiza o armazenamento do contrato chamado Atualiza o armazenamento do contrato chamador
Interação de Contrato Pode chamar contratos externos, bibliotecas e funções dentro do mesmo contrato Pode chamar contratos externos, bibliotecas e funções dentro do mesmo contrato
Funcionalidade Herdada Não herda o armazenamento ou contexto do contrato chamador Herda o armazenamento e contexto do contrato chamador
Gás e Custo Usa a provisão de gás fornecida, e qualquer gás restante é devolvido Usa a provisão de gás fornecida, e qualquer gás restante é devolvido

Conclusões

Em conclusão, entender as diferenças entre call e delegatecall é crucial para os desenvolvedores Solidity. call preserva o armazenamento e o contexto do chamador, enquanto delegatecall modifica o armazenamento do chamador. É importante, tratar adequadamente os valores de retorno e considerar medidas de segurança ao usar esses mecanismos de invocação de função. Ao entender esses conceitos, os desenvolvedores podem usar efetivamente call e delegatecall em seus contratos inteligentes, ao mesmo tempo que mitigam os riscos de segurança. Sempre siga as melhores práticas e consulte o guia de Melhores Práticas de Contrato Inteligente da Ethereum para obter mais informações sobre o desenvolvimento seguro de Solidity.

Referências:

Ethereum Smart Contract Best Practices

Delegatecall vulnerabilities in Solidity

yAcademy Proxies Research

Artigo original publicado por Sergio Mazariego. Traduzido por Paulinho Giovannini.

Top comments (0)