Eu recentemente tenho revisado meus conhecimentos em Solidity para reforçar os detalhes, e estou escrevendo um "Guia WTF de Introdução Simples ao Solidity" para iniciantes (programadores experientes podem buscar outros tutoriais), com atualizações semanais de 1 a 3 lições.
Twitter: @0xAA_Science
Comunidade: Discord | Grupo WeChat | Site oficial wtf.academy
Todo código e tutorial estão disponíveis no github: github.com/AmazingAng/WTF-Solidity
Nós já introduzimos o uso do call para enviar ETH na Liçăo 20: Enviando ETH. Nesta lição, vamos explorar como utilizar o call para chamar funções de contratos.
O call é uma função de membro de tipo address que permite a interação com outros contratos. Seu retorno é do tipo (bool, bytes memory), indicando respectivamente se a chamada foi bem-sucedida e o valor de retorno da função alvo.
- O
callé recomendado oficialmente pela Solidity para enviarETHativando funçõesfallbackoureceive. - Não é recomendado usar o
callpara chamar funções de outros contratos, pois ao fazer isso, você está dando controle ao contrato alvo. A maneira recomendada é declarar a variável do contrato e chamar a função, conforme visto na Liçăo 21: Chamar Contrato. - Quando não temos o código fonte ou o
ABIdo contrato alvo, não podemos criar a variável do contrato; nesse caso, ainda podemos chamar a função do contrato alvo utilizando ocall.
As regras de uso do call são as seguintes:
enderecoContratoAlvo.call(bytecode);
O bytecode é obtido usando a função de codificação estruturada abi.encodeWithSignature:
abi.encodeWithSignature("nomeDaFuncao(tipoParametro)", parametrosSeparadosPorVírgula)
O "nomeDaFuncao(tipoParametro)" é a "assinatura da função" como por exemplo abi.encodeWithSignature("f(uint256,address)", _x, _addr).
Além disso, ao chamar o contrato utilizando o call, é possível especificar a quantidade de ETH e de gas a enviar na transação:
enderecoContratoAlvo.call{value:valor, gas:quantidadeGas}(bytecode);
Parece um pouco complexo, então vamos ver um exemplo de aplicação do call.
Primeiramente, vamos escrever um contrato simples chamado OtherContract e implantá-lo. O código é praticamente o mesmo da lição 21, com a adição de uma função fallback.
contract OtherContract {
uint256 private _x = 0; // variável de estado x
// evento para quando recebe ETH, registra o valor e o gas
event Log(uint amount, uint gas);
fallback() external payable{}
// retorna o saldo de ETH do contrato
function getBalance() view public returns(uint) {
return address(this).balance;
}
// função ajusta o valor da variável de estado _x e pode enviar ETH para o contrato (pagável)
function setX(uint256 x) external payable{
_x = x;
// se enviar ETH, dispara o evento Log
if(msg.value > 0){
emit Log(msg.value, gasleft());
}
}
// lê o valor de x
function getX() external view returns(uint x){
x = _x;
}
}Este contrato possui uma variável de estado x, um evento Log que é acionado ao receber ETH, e três funções:
getBalance(): retorna o saldo de ETH do contrato.setX(): funçãoexternal payableque permite ajustar o valor dexe enviarETHpara o contrato.getX(): retorna o valor dex.
Vamos criar uma função Response para chamar as funções do contrato alvo. Primeiro, definimos um evento Response que exibe o success e data da chamada, permitindo-nos verificar os resultados.
// Definir evento Response para exibir o sucesso e os dados da chamada
event Response(bool success, bytes data);Definimos a função callSetX para chamar a função setX() do contrato alvo, enviando a quantidade de ETH recebida e emitindo o evento Response para exibir o success e data.
function callSetX(address payable _addr, uint256 x) public payable {
// Chamando setX() e enviando ETH
(bool success, bytes memory data) = _addr.call{value: msg.value}(
abi.encodeWithSignature("setX(uint256)", x)
);
emit Response(success, data); // Emite o evento
}Em seguida, chamamos o callSetX para definir a variável _x como 5, passando o endereço do contrato OtherContract e o valor 5. Como a função alvo setX() não possui valor de retorno, o data retornado no evento Response será 0x, o que representa um valor vazio.
Agora, vamos chamar a função getX(), que retornará o valor da variável _x do contrato alvo. Utilizamos o abi.decode para decodificar o data retornado pelo call e obter o valor numérico.
function callGetX(address _addr) external returns(uint256){
// Chamando getX()
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("getX()")
);
emit Response(success, data); // Emite o evento
return abi.decode(data, (uint256));
}O valor de retorno do getX() é mostrado no evento Response, sendo representado em hexadecimal (0x0000000000000000000000000000000000000000000000000000000000000005). Depois de decodificar esse valor, obtemos o número 5.
Se chamarmos uma função que não existe no contrato alvo, o fallback do contrato será acionado.
function callNonExist(address _addr) external{
// Chamando uma função inexistente
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("foo(uint256)")
);
emit Response(success, data); // Emite o evento
}Neste exemplo, chamamos a função inexistente foo. O call ainda é bem-sucedido e retorna success, no entanto, na verdade, está chamando a função de fallback do contrato alvo.
Nesta lição, aprendemos como usar o call, uma função de baixo nível, para chamar funções de outros contratos. Embora não seja a maneira recomendada de chamar contratos devido aos riscos de segurança, o call é útil quando não temos o código fonte ou o ABI do contrato alvo.