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.
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
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çãocall
para invocar a funçãosetValue
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çãodelegatecall
para invocar a funçãosetValue
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
Chamando uma função de outro contrato, enviando valor (ETH)
Chamando uma função com um argumento
Chamando uma função com dois argumentos do mesmo tipo de dados
Chamando uma função com dois argumentos de diferentes tipos de dados
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
:
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
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 dedelegatecall
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
Artigo original publicado por Sergio Mazariego. Traduzido por Paulinho Giovannini.
Latest comments (0)