Recentemente, tenho revisitado o Solidity para consolidar alguns detalhes e escrever um "Guia de Introdução ao Solidity" para iniciantes (os mestres em programação podem procurar por outro tutorial), com atualizações semanais de 1 a 3 artigos.
Twitter: @0xAA_Science
Comunidade: Discord | Grupo no WhatsApp | Site oficial wtf.academy
Todo o código e tutorial estão disponíveis no GitHub: github.com/AmazingAng/WTF-Solidity
O delegatecall
é uma função de baixo nível do tipo address
em Solidity, semelhante ao call
. Se delegate
significa delegação/representação, então o que o delegatecall
está delegando?
Quando o usuário A
chama o contrato B
para chamar o contrato C
, a função executada é a do contrato C
, e o contexto
é do contrato C
: msg.sender
é o endereço de B
, e se a função alterar variáveis de estado, os efeitos serão aplicados às variáveis do contrato C
.
Enquanto, ao usar o delegatecall
do usuário A
no contrato B
para chamar o contrato C
, a função executada ainda é a do contrato C
, mas o contexto
continua sendo do contrato B
: msg.sender
é o endereço de A
, e as alterações nas variáveis de estado serão aplicadas ao contrato B
.
Podemos entender da seguinte maneira: um investidor (usuário A
) confia seus ativos (variáveis de estado do contrato B
) a um agente de investimento de risco (contrato C
) para administrar. A função executada é do agente de investimento de risco, mas as alterações afetam os ativos.
A sintaxe do delegatecall
é semelhante à do call
:
enderecoDoContratoAlvo.delegatecall(códigoBinário);
O códigoBinário
é obtido utilizando a função de codificação estruturada abi.encodeWithSignature
:
abi.encodeWithSignature("assinaturaDaFunção", parâmetrosSeparadosPorVírgulas)
A assinaturaDaFunção
é "nomeDaFunção(tiposDeParâmetrosSeparadosPorVírgulas)"
, por exemplo, abi.encodeWithSignature("f(uint256,address)", _x, _addr)
.
Diferentemente do call
, o delegatecall
pode definir a quantidade de gas
a ser enviado na transação, mas não pode definir a quantidade de ETH.
Atenção: O
delegatecall
possui riscos de segurança. Deve-se garantir que a estrutura de armazenamento de variáveis de estado dos contratos atual e de destino seja a mesma, e que o contrato de destino seja seguro, caso contrário, pode resultar em perda de ativos.
Atualmente, o delegatecall
é principalmente utilizado em dois cenários:
-
Contrato de Procuração (
Proxy Contract
): separa o armazenamento de variáveis e a lógica do contrato. O contrato de Procuração (Proxy Contract
) armazena todas as variáveis pertinentes e mantém o endereço da lógica do contrato; todas as funções estão dentro do contrato de lógica (Logic Contract
), sendo executadas por meio dedelegatecall
. Para atualizar, basta direcionar o contrato de Procuração para o novo contrato de lógica. -
Diamantes EIP-2535: Diamantes são contratos de procuração com múltiplos contratos de implementação. Para mais informações, consulte: Introdução ao Padrão Diamante.
Chamada: Você (A
) chama o contrato B
para chamar o contrato de destino C
.
Primeiramente, vamos escrever um contrato de destino C
simples: com duas variáveis públicas num
e sender', sendo
uint256e
address, respectivamente; uma função que define
numcomo o valor recebido em
_nume define
sendercomo
msg.sender`.
// Contrato de Destino C
contract C {
uint public num;
address public sender;
function setVars(uint _num) public payable {
num = _num;
sender = msg.sender;
}
}
Em seguida, o contrato B
deve ter a mesma estrutura de armazenamento de variáveis do contrato de destino C
, com duas variáveis e na mesma ordem: num
e sender
.
contract B {
uint public num;
address public sender;
}
Agora, vamos chamar a função setVars
do contrato C
usando call
e delegatecall
para entender melhor as diferenças.
A função callSetVars
chama o setVars
usando call
. Ela recebe dois parâmetros, _addr
e _num
, correspondentes ao endereço do contrato C
e ao parâmetro do setVars
.
// Chama o setVars() do contrato C usando a função call, modificando as variáveis de estado do contrato C
function callSetVars(address _addr, uint _num) external payable {
// chamar setVars()
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
Já a função delegatecallSetVars
chama o setVars
usando delegatecall
. Assim como a função callSetVars
, ela recebe dois parâmetros, _addr
e _num
, correspondentes ao endereço do contrato C
e ao parâmetro do setVars
.
// Chama o setVars() do contrato C usando delegatecall, modificando as variáveis de estado do contrato B
function delegatecallSetVars(address _addr, uint _num) external payable {
// delegatecall setVars()
(bool success, bytes memory data) = _addr.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
-
Primeiro, implantamos os contratos
B
eC
. -
Após a implantação, verificamos os valores iniciais das variáveis de estado do contrato
C
, que também são os mesmos para o contratoB
. -
Em seguida, executamos a função
callSetVars
do contratoB
, passando o endereço do contratoC
e10
. -
Após a execução, as variáveis de estado do contrato
C
são alteradas:num
é definido como10
esender
é o endereço do contratoB
. -
Agora, executamos a função
delegatecallSetVars
do contratoB
, passando o endereço do contratoC
e100
. -
Devido ao
delegatecall
, o contexto é do contratoB
. Após a execução, as variáveis de estado do contratoB
serão alteradas:num
é definido como100
esender
é o endereço da sua carteira. As variáveis de estado do contratoC
não serão modificadas.
Neste artigo, exploramos a função delegatecall
, mais um recurso de baixo nível em Solidity. Semelhante ao call
, o delegatecall
é usado para chamar outros contratos, com a diferença sendo o contexto de execução: B chama C
, o contexto é C
; enquanto B delegatecall C
, o contexto é B
. Atualmente, a principal aplicação do delegatecall
é em contratos de procuração e nos Diamantes EIP-2535.