Neste artigo, você terá uma visão geral abrangente do delegatecall em Solidity, como funciona e exemplos reais de seus casos de uso. Também passaremos por um guia passo a passo para usar o delegatecall com uma análise detalhada da EVM, destacando armadilhas comuns e como evitá-las. Sem mais delongas, vamos começar.
Pré-requisito: Antes de se aprofundar neste artigo, certifique-se de que você compreenda como funciona o layout de armazenamento de variáveis de estado em Solidity. Caso contrário, certifique-se de verificar a documentação sobre isso https://docs.soliditylang.org/en/v0.8.19/internals/layout_in_storage.html
O que é Delegatecall?
Em Solidity, o delegatecall
é uma função de baixo nível que permite que um contrato delegue sua chamada a outro contrato (emprestando a funcionalidade de outro contrato) enquanto ainda preserva seu próprio armazenamento e contexto. Quando um contrato faz um delegatecall
, o código no endereço de destino é executado no contexto do contrato que chama. Isso significa que o armazenamento, as variáveis de estado e as funções do contrato que chama estão acessíveis ao código em execução.
Para entender melhor como o delegatecall
funciona, vamos considerar o seguinte cenário:
Imagine que você é um empreiteiro👷 e foi contratado para construir uma casa🏠. Você é responsável por coordenar com vários outros empreiteiros que serão responsáveis por diferentes aspectos do projeto, como encanamento, elétrica, carpintaria, etc.
Neste cenário, você é como o contrato que chama e os outros empreiteiros são como os contratos que você chamará usando o delegatecall
.
Agora, imagine que você precise do encanador para entrar e instalar os canos na casa. Em vez de fazer isso você mesmo, você pode usar o delegatecall
para chamar o encanador e fazê-lo instalar os canos para você. Quando você usa o delegatecall
, o encanador vem ao seu canteiro de obras e instala os canos no contexto do seu projeto, em vez do dele. Isso significa que o encanador pode acessar as ferramentas, materiais e trabalhadores que você tem disponíveis, em vez de ter que trazer os próprios. No entanto, o encanador ainda usará sua própria experiência e conhecimento para instalar os canos corretamente.
Da mesma forma, quando você usa o delegatecall
em um contrato inteligente, pode chamar outro contrato e executar seu código no contexto do seu próprio contrato. Isso permite que você acesse seu próprio armazenamento, variáveis de estado e funções enquanto ainda é capaz de executar o código do outro contrato.
A Diferença entre Call e DelegateCall
Vamos fazer uma pausa por um momento e examinar a distinção entre utilizar call
e delegatecall
em Solidity. É importante compreender a diferença entre esses dois, pois ela é crucial para atualizar contratos inteligentes.
call
é uma função de baixo nível do Solidity usada para fazer uma chamada externa. Quando call
é usado, o contrato alvo é executado em seu próprio contexto e qualquer alteração de estado que ele faça é limitada ao seu próprio armazenamento. call
também retorna um valor booleano para indicar se a chamada foi bem-sucedida ou não.
Já delegatecall
é similar ao call
, mas, em vez de executar o contrato alvo em seu próprio contexto, ele o executa no contexto do estado atual do contrato chamador, como você pode ver no diagrama acima. Isso significa que qualquer alteração de estado feita pelo contrato alvo é feita no armazenamento do contrato chamador. Esse caso de uso específico tem grande importância ao atualizar um contrato, pois permite separar a lógica do contrato em vários contratos sem perder o estado original do contrato.
Por exemplo, suponha que você tenha um contrato que precisa ser atualizado. Você pode criar um novo contrato com a lógica atualizada e usar
delegatecall
para executar o código do novo contrato no contexto do armazenamento do contrato original. Dessa forma, a lógica atualizada pode acessar e modificar o estado do contrato original conforme necessário.
Coisas a serem observadas ao usar delegatecall
Quando um contrato faz um
delegatecall
, o valor deaddress(this)
,msg.sender
emsg.value
não alteram seus valores. No entanto, é importante notar que, embora o contrato alvo possa acessar as variáveis de armazenamento do contrato chamador, ele não herda os parâmetros de chamada de mensagem originais do contrato chamador. Os parâmetros de chamada de mensagem originais incluemmsg.sender
, que é o endereço da conta que enviou a chamada de mensagem original, emsg.value
, que é a quantidade de Ether enviada junto com a chamada de mensagem, se houver.Quando um contrato é chamado usando
delegatecall
, os valores demsg.sender
emsg.value
passados para o contrato alvo não são aqueles dodelegatecall
, mas sim aqueles da chamada de mensagem original para o contrato chamador. Isso significa que o contrato alvo não pode acessar o endereço real da conta que enviou odelegatecall
ou a quantidade de Ether enviada com odelegatecall
.
Para ilustrar este ponto, vamos supor que o Contrato A chame o Contrato B usando
delegatecall
. Se o Contrato B acessarmsg.sender
oumsg.value
, ele recuperará os valores da chamada de mensagem original para o Contrato A, não aqueles dodelegatecall
do Contrato A para o Contrato B. Isso ocorre porquedelegatecall
encaminha toda a mensagem (incluindomsg.sender
emsg.value
) para o contrato alvo, mas executa o código do contrato chamado no contexto do contrato chamador.
- Quando uma função é executada com
delegatecall
, leituras e gravações nas variáveis de estado não acontecem no contrato que carrega e executa a função, ou seja, o contrato alvo. Em vez disso, as leituras e gravações acontecem no contrato que define a função sendo executada (contrato chamador). Isso ocorre porque o contexto de execução é o do contrato alvo, não o do contrato chamador. Portanto, quaisquer alterações feitas nas variáveis de estado dentro da função alvo acontecerão no armazenamento do contrato alvo, não no contrato chamador.
Por exemplo, quando o Contrato A usa
delegatecall
para executar uma função do Contrato B, o contexto de execução ainda está dentro do Contrato A. Isso significa que qualquer leitura ou gravação de variáveis de estado dentro desse contexto será para as variáveis de estado do Contrato A, não do Contrato B. As variáveis de estado do Contrato B estão efetivamente "ocultas" do contexto de execução do Contrato A e não podem ser lidas ou gravadas diretamente. No entanto, as funções do Contrato B ainda podem acessar e modificar suas próprias variáveis de estado, desde que sejam chamadas diretamente (ou seja, não por meio dedelegatecall
) ou por meio de outro contrato que usacall
em vez dedelegatecall
.
Como usar delegatecall com exemplo de código
No código abaixo, o CallingContract
possui o seguinte:
- Uma variável de estado configurada como string "Something".
- Uma função
set
que recebe um endereço_calledAddress
como argumento. Este endereço deve corresponder ao endereço do contratoTargetContract
. A funçãoset
então usadelegatecall
para chamar a funçãochangeMessage
doTargetContract
, passando o argumento "He is Alive" (“Ele está Vivo”) como uma string quando chamado. - A função
changeMessage
doTargetContract
atualiza as variáveis de estadomessage
eowner
, e as alterações são refletidas noCallingContract
. - Se a função
delegatecall
falhar, a funçãoset
será revertida e retornará uma mensagem de erro. A mensagem de erro indicará se a chamada da função foi revertida ou se houve outro erro.
O TargetContract
, que possui a funcionalidade a ser implementada pelo CallingContract
usando delegatecall
, possui o seguinte:
- O contrato tem duas variáveis de estado,
message
eowner
, ambas declaradas como públicas, o que significa que podem ser acessadas por qualquer pessoa. A variávelmessage
é inicializada com a string "I love solidity" (“Eu amo solidity”), enquanto a variávelowner
é inicializada com o valor passado ao construtor quando o contrato é implantado. - O construtor do contrato recebe um argumento
_owner
, que é um endereço que representa o proprietário do contrato. Este argumento é usado para inicializar a variávelowner
. - O contrato também possui uma função chamada
changeMessage
, que recebe um argumento de string_msg
e atualiza as variáveismessage
eowner
. A variávelmessage
é configurada para o valor de_msg
, enquanto a variávelowner
é configurada para o endereço do chamador da função, que é obtido usandomsg.sender
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract CallingContract {
string public message = "Something";
address public owner = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
function set(address _calledAddress) external {
(bool success, bytes memory returndata) = _calledAddress.delegatecall(
abi.encodeWithSelector(
TargetContract.changeMessage.selector,
"He is Alive" // Ele está vivo
)
);
// se a chamada de função for revertida
if (success == false) {
// se houver uma string de razão de retorno
if (returndata.length > 0) {
// borbulhe qualquer motivo para reverter
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert("Chamada de função revertida");
}
}
}
}
contract TargetContract {
string public message = "I love solidity"; // Eu amo solidity
address public owner; //0x637CcDeBB20f849C0AA1654DEe62B552a058EA87
//address(this) => 0x4401f7E80aDB3D7589E720e069CB7F81E0402550
constructor(address _owner) {
owner = _owner;
}
function changeMessage(string calldata _msg) external {
message = _msg;
owner = msg.sender;
}
}
O TargetContract
tem o seguinte quando a função changeMessage()
é chamada com um novo argumento:
- address(this) -> 0x4401f7E80aDB3D7589E720e069CB7F81E0402550
- msg.sender -> 0x637CcDeBB20f849C0AA1654DEe62B552a058EA87
- message(passe qualquer argumento de string que você quiser) -> "Just updated" (“Apenas Atualize”)
Antes de a função set()
no CallingContract
ser chamada, o contrato tem o seguinte:
- address(this) -> 0xB82a6d6aAFbeB1d994604C119B97272Cba504F4F
- owner -> 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
- message -> "Something" (“Algo”)
Depois que a função set()
no CallingContract
é chamada, o contrato tem o seguinte:
- address(this) -> 0xB82a6d6aAFbeB1d994604C119B97272Cba504F4F
- msg.sender -> 0xEB7A41D324ee4859E3cbFAd4b3820B82FCCe6658
- message -> "He is Alive" (“Ele está Vivo”)
Se você observar, o valor de address(this)
não mudou e, como a variável owner foi atribuída ao chamador da função set()
, que é o msg.sender, a variável de estado CallingContract
é atualizada com o msg.sender
atual no contexto do CallingContract
e não do TargetContract
.
A variável "owner" no TargetContract
ainda retorna um valor de 0x637CcDeBB20f849C0AA1654DEe62B552a058EA87, que é o mesmo que o msg.sender
que implantou o TargetContract
. Ao chamar a variável "owner" no CallingContract
, ela retorna o valor do msg.sender
que chamou a função "set()", que é "0xEB7A41D324ee4859E3cbFAd4b3820B82FCCe6658" neste contexto.
Assim, todos os requisitos mencionados em "Coisas a observar ao usar delegatecall
" foram cumpridos.
Armadihas comuns a evitar ao usar delegatecall no Solidity:
Usar
delegatecall
para chamar um contrato desconhecido ou malicioso: Se você estiver usandodelegatecall
para chamar um contrato com o qual não está familiarizado, há um risco de que o contrato possa se comportar de maneira maliciosa ou ter vulnerabilidades que possam ser exploradas. Sempre se certifique de auditar cuidadosamente qualquer contrato que você esteja chamando comdelegatecall
.Gerenciar o layout da variável de estado entre o contrato de chamada e o contrato alvo: Quando uma função é chamada com
delegatecall
, ela opera no contexto do armazenamento do contrato de chamada, o que significa que compartilha o mesmo espaço de endereço da variável de estado (posição do slot). Portanto, é importante que o contrato de chamada e o contrato alvo tenham o mesmo layout de variável de estado para quaisquer variáveis de estado que sejam lidas ou gravadas por ambos os contratos. Isso garante que ambos os contratos acessem as mesmas variáveis de estado na mesma ordem, evitando problemas como sobrescrever ou interpretar incorretamente as variáveis de estado um do outro. Ao ter o mesmo layout de variável de estado, ambos os contratos podem acessar os mesmos dados nas mesmas localizações de memória, independentemente de seus detalhes de implementação individuais.
Por exemplo, digamos que o Contract A declare variáveis de estado 'string message;' e 'address owner;' e o Contract B declare variáveis de estado 'uint256 num;' e 'address owner;'. Eles têm variáveis de estado diferentes na posição 0 ('string message' e 'uint256 num') no armazenamento do contrato e, portanto, escreverão e lerão dados incorretos entre eles na posição 0 se
delegatecall
for usado entre eles.
É importante planejar e gerenciar cuidadosamente o layout da variável de estado dos contratos que usam delegatecall
para garantir que eles estejam acessando os mesmos dados nas mesmas localizações de memória. Isso pode ser alcançado por meio de estratégias como a definição de uma interface compartilhada ou comunicando minuciosamente qualquer alteração no layout da variável de estado. Para saber mais sobre como usar um armazenamento compartilhado para contratos atualizáveis, confira meu artigo anterior sobre o Diamond Standard: https://medium.com/@ajaotosinserah/upgradable-functionality-with-eip2535-a-comparative-analysis-c9c1d9954296
Alterando variáveis de estado no contrato de chamada: Ao usar
delegatecall
, você precisa ter cuidado para não modificar acidentalmente as variáveis de estado no contrato de chamada. Se a função chamada modificar variáveis de estado, essas alterações serão feitas no estado do contrato de chamada em vez do estado do contrato alvo. Sempre se certifique de usarcall
em vez dedelegatecall
se precisar modificar variáveis de estado no contrato de chamada.É importante observar que, se
delegatecall
for chamado em um endereço que não seja um contrato (EOA) e, portanto, não tenha código, ele retornará um valor de statustrue
(verdadeiro). Esse comportamento pode causar bugs se o código esperar que as funçõesdelegatecall
retornemfalse
(falso) quando não puderem ser executadas. Para evitar esse problema, é aconselhável verificar se qualquer variável de endereço usada comdelegatecall
contém código antes de executar a função. Se houver alguma incerteza sobre se o endereço sempre conterá código, é importante verificar antes de usardelegatecall
e reverter a transação se o endereço não contiver código. Aqui está um exemplo de código que verifica se um endereço possui código:
function hasCode(address _address) internal view returns (bool) {
uint256 codeSize;
assembly { codeSize := extcodesize(_address) }
return codeSize > 0;
}
// Esta função utiliza o opcode extcodesize para verificar o tamanho do código no endereço fornecido.
// Se o tamanho do código for maior que zero, o endereço contém código e a função retorna verdadeiro.
// Caso contrário, retorna falso. Ao usar esta função para verificar a presença de código em um endereço
// antes de usar delegatecall, você pode ajudar a prevenir bugs e vulnerabilidades no seu código.
-
Utilização incorreta do valor de retorno:
delegatecall
retorna um valor booleano indicando se a chamada foi bem-sucedida ou não. No entanto, o valor de retorno da função chamada não é automaticamente retornado. Isso pode levar a comportamentos inesperados se o valor de retorno não for tratado adequadamente. Sempre se certifique de lidar corretamente com o valor de retorno da função chamada. -
Passando argumentos incorretos: ao usar
delegatecall
, você precisa garantir que os argumentos passados para a função chamada estejam corretos. Se os argumentos estiverem incorretos, a função chamada pode se comportar de maneira inesperada ou até mesmo falhar. Sempre se certifique de passar os argumentos corretos para a função chamada.
Para evitar essas armadilhas, é importante entender completamente como delegatecall
funciona e usá-lo com critério. Certifique-se de testar seu código completamente e auditar todos os contratos que você está chamando com delegatecall
.
Passeio pela EVM
Na discussão a seguir, revisaremos brevemente os opcodes usados no exemplo de código anterior e forneceremos uma explicação de como o OPCODE de delegatecall
é utilizado, bem como o preço do gás.
146 JUMP
147 JUMPDEST
148 PUSH1 40
150 MLOAD
151 PUSH2 0082
154 SWAP2
155 SWAP1
156 PUSH2 02a3
159 JUMP
160 JUMPDEST
161 PUSH1 40
163 DUP1
164 MLOAD
165 PUSH1 20
167 PUSH1 24
169 DUP3
170 ADD
171 DUP2
172 SWAP1
173 MSTORE
174 PUSH1 0b
176 PUSH1 44
178 DUP4
179 ADD
180 MSTORE
181 PUSH11 486520697320416c697665
193 PUSH1 a8
195 SHL
196 PUSH1 64
198 DUP1
199 DUP5
200 ADD
201 SWAP2
202 SWAP1
203 SWAP2
204 MSTORE
205 DUP4
206 MLOAD
207 DUP1
208 DUP5
209 SUB
210 SWAP1
211 SWAP2
212 ADD
213 DUP2
214 MSTORE
215 PUSH1 84
217 SWAP1
218 SWAP3
219 ADD
220 DUP4
221 MSTORE
222 DUP2
223 ADD
224 DUP1
225 MLOAD
226 PUSH1 01
228 PUSH1 01
230 PUSH1 e0
232 SHL
233 SUB
234 AND
235 PUSH4 60fd1c4f
240 PUSH1 e0
242 SHL
243 OR
244 SWAP1
245 MSTORE
246 SWAP1
247 MLOAD
248 PUSH1 00
251 DUP3
252 SWAP2
253 PUSH1 01
255 PUSH1 01
257 PUSH1 a0
259 SHL
260 SUB
261 DUP6
262 AND
263 SWAP2
264 PUSH2 0110
267 SWAP2
268 PUSH2 02d6
271 JUMP
272 JUMPDEST
273 PUSH1 00
275 PUSH1 40
277 MLOAD
278 DUP1
279 DUP4
280 SUB
281 DUP2
282 DUP6
283 GAS
284 DELEGATECALL
285 SWAP2
286 POP
287 POP
Os OPCODES acima representam as instruções de baixo nível executadas pela EVM quando a função set()
foi chamada no contrato CallingContract
. Aqui está uma breve explicação de cada opcode:
-
JUMP
(opcode 146): Este opcode é usado para implementar o fluxo de controle no bytecode. -
PUSH1
(opcode 148, 161, 181, 215 e 273) e PUSH2 (opcode 151, 156, 267 e 268): Esses opcodes empurram um valor constante para a pilha. Os valores constantes são usados para endereços de memória e deslocamentos no bytecode. -
MLOAD
(opcode 150 e 164) eMSTORE
(opcode 173, 180, 213 e 221): Esses opcodes são usados para ler e escrever dados de e para a memória. -
SWAP1
,SWAP2
,SWAP3
eDUP1
(opcodes 154, 155, 172, 206, 207, 208, 212, 217, 222, 223, 225, 228, 229, 231, 252, 253 e 280): Esses opcodes são usados para manipular a pilha. -
ADD
(opcodes 169, 211, 213, 223, 260 e 262) eSUB
(opcodes 209, 251 e 262): Esses opcodes são usados para realizar operações aritméticas. -
GAS
(opcode 283): Este opcode empurra todo o gás disponível para a pilha. É usado para limitar a quantidade de gás disponível para a operação dedelegatecall
. -
DELEGATECALL
(opcode 284): Este opcode executa a operaçãodelegatecall
de fato, transferindo o controle para o contrato chamado enquanto preserva o armazenamento do contrato chamador e executa no contexto do contrato chamador.
Preço do Gás
O preço do gás para um opcode delegatecall
é calculado da mesma forma que para qualquer outro opcode na Máquina Virtual Ethereum (EVM). É determinado pelo preço atual do gás na rede multiplicado pelo limite de gás para a transação na qual o opcode delegatecall
está incluído.
O preço do gás para um opcode delegatecall
é o custo do gás para executar o código no contrato de destino, além de qualquer gás adicional necessário para o próprio delegatecall
. Este custo de gás é deduzido do limite de gás do contrato chamador, e qualquer gás restante é retornado ao contrato chamador após a conclusão do delegatecall
.
Ao usar delegatecall
, é importante notar que o custo do gás não é determinado apenas pelo código sendo executado no contrato de destino, mas também pelo estado do contrato chamador. Isso significa que, se o estado do contrato chamador for complexo, pode ser necessário mais gás para executar o opcode delegatecall
.
Portanto, é importante gerenciar cuidadosamente os custos de gás ao usar delegatecall
para evitar ficar sem gás durante a execução. Isso pode ser feito estimando o custo do gás do delegatecall
com antecedência, usando código eficiente em termos de gás e layouts de armazenamento, e testando completamente o delegatecall
em um ambiente de teste antes de implantá-lo na rede principal.
Conclusão
Delegatecall
é um recurso do Solidity que permite que contratos executem código de outros contratos, mantendo o armazenamento e o estado do contrato chamador. Este artigo abordou como o delegatecall
funciona, seus benefícios e um caso de uso real. Também fornecemos um guia passo a passo para usar o delegatecall
e destacamos armadilhas comuns a serem evitadas, incluindo gerenciamento de layout de variáveis de estado, tratamento adequado de exceções e gerenciamento de custos de gás. Ao usar o delegatecall
com cuidado, os desenvolvedores de Solidity podem aumentar a reutilização de código e criar contratos inteligentes mais modulares e flexíveis. Obrigado por ler!
Esse artigo é uma tradução feita por @bananlabs. Você pode encontrar o artigo original aqui
Oldest comments (0)