Call, Fallback, Delegatecall e chamadas de contratos

porMatheusem22/05/2022

Nesse artigo iremos abordar sobre a utilização das funções call, fallback, delegatecall e chamadas de outros contratos no seu contrato inteligente. Falaremos sobre call, fallback e delegatecall.

Call (Chamar)

call é uma função de baixo nível para interagir com outros contratos.

Este é o método recomendado para usar quando você está apenas enviando Ether por meio da chamada da função fallback.

No entanto, não é a maneira recomendada de chamar funções existentes.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// Definição do contrato Receiver ou Recebedor
contract Receiver {
event Received(address caller, uint amount, string message);
fallback() external payable {
emit Received(msg.sender, msg.value, "Fallback foi chamado");
}
function foo(string memory _message, uint _x) public payable returns (uint) {
emit Received(msg.sender, msg.value, _message);
return _x + 1;
}
}
// Definição do contrato Caller ou Chamador
contract Caller {
event Response(bool success, bytes data);
// Vamos imaginar que o contrato B não tenha o código fonte para
// contrato A, mas sabemos o endereço de A e a função a ser chamada.
function testCallFoo(address payable _addr) public payable {
// Você pode enviar éter e especificar uma quantidade de gás personalizada
(bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
);
emit Response(success, data);
}
// Chamar uma função que não existe aciona a função de fallback.
function testCallDoesNotExist(address _addr) public {
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("doesNotExist()")
);
emit Response(success, data);
}
}

Fallback (Retorno)

fallback é uma função que não recebe nenhum argumento e não retorna nada.

É executada Quando

  • uma função que não existe é chamada
  • o Ether enviado diretamente para um contrato, mas receive() não existe ou msg.data não está vazio

fallback tem um limite de gas de 2300 quando chamado por transfer ou send.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Fallback {
event Log(uint gas);
// A função de fallback deve ser declarada como externa.
fallback() external payable {
// send / transfer (envia 2300 gas para esta função fallback)
// call (envia todo gas)
emit Log(gasleft());
}
// Função auxiliar para verificar o saldo deste contrato
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
contract SendToFallback {
function transferToFallback(address payable _to) public payable {
_to.transfer(msg.value);
}
function callFallback(address payable _to) public payable {
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Falha ao enviar Ether");
}
}

Delegatecall

delegatecall é uma função de baixo nível semelhante a call.

Quando o contrato A é executado delegatecall para o contrato B, o código de B é executado com o armazenamento do contrato A, msg.sender e msg.value.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// NOTA: Implemente este contrato primeiro
contract B {
// NOTA: layout de armazenamento deve ser o mesmo do contrato A
uint public num;
address public sender;
uint public value;
function setVars(uint _num) public payable {
num = _num;
sender = msg.sender;
value = msg.value;
}
}
contract A {
uint public num;
address public sender;
uint public value;
function setVars(address _contract, uint _num) public payable {
// O armazenamento de A está definido, B não é modificado.
(bool success, bytes memory data) = _contract.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
}

Seletor de Função

Quando uma função é chamada, os primeiros 4 bytes calldata especificam qual função chamar.

Esses 4 bytes são chamados de seletor de função.

Por exemplo, este código abaixo. Ele usa call para executar transfer um contrato no endereço addr.

addr.call(abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123))

Os primeiros 4 bytes retornados abi.encodeWithSignature(....) são o seletor de função.

Posso economizar uma pequena quantidade de gas se pré-computar e inserir o seletor de função em seu código?

Aqui está como o seletor de função é calculado.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract FunctionSelector {
/*
"transfer(address,uint256)"
0xa9059cbb
"transferFrom(address,address,uint256)"
0x23b872dd
*/
function getSelector(string calldata _func) external pure returns (bytes4) {
return bytes4(keccak256(bytes(_func)));
}
}

Chamando outro contrato

Um contrato pode chamar outros contratos de 2 maneiras.

A maneira mais fácil é apenas chamá-lo da seguinte maneira: A.foo(x, y, z).

Outra maneira de chamar outros contratos é usar o call. Este método não é o mais recomendado.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// Contrato a ser chamado
contract Callee {
uint public x;
uint public value;
function setX(uint _x) public returns (uint) {
x = _x;
return x;
}
function setXandSendEther(uint _x) public payable returns (uint, uint) {
x = _x;
value = msg.value;
return (x, value);
}
}
// Contrato que realizará as chamadas
contract Caller {
// Dessa forma passamos o contrato Callee por parâmetro
// através do _callee conseguimos acessar todas as funções
// públicas do contrato Callee
function setX(Callee _callee, uint _x) public {
uint x = _callee.setX(_x);
}
function setXFromAddress(address _addr, uint _x) public {
// Dessa forma, definimos o contrato Callee dentro
// da função, passando o endereço por parâmetro no
// contrato Callee e então podemos chamar suas funções
Callee callee = Callee(_addr);
callee.setX(_x);
}
function setXandSendEther(Callee _callee, uint _x) public payable {
(uint x, uint value) = _callee.setXandSendEther{value: msg.value}(_x);
}
}

Testar no Remix