Un contrato en Solidity puede llamar a otros contratos a través de dos métodos: mediante la interfaz del contrato, lo cual se considera una llamada de alto nivel, o usando el método call, que es un enfoque de bajo nivel.
A pesar de que ambos métodos utilizan el opcode CALL, Solidity los maneja de manera diferente.
En este artículo, compararemos ambos: por qué una llamada de bajo nivel nunca revierte, mientras que una llamada de alto nivel puede revertir, y por qué una llamada de bajo nivel a una dirección vacía se considera exitosa, mientras que una llamada de alto nivel revierte al llamar a un contrato inexistente.
Por qué una llamada de bajo nivel (o un delegatecall) nunca revierte pero una llamada a través de la interfaz del contrato sí puede revertir
Antes de explicar por qué, permítanme citar la documentación de Solidity que aborda este problema.
Cuando ocurren excepciones en una sub-llamada, estas “burbujean” (es decir, las excepciones se vuelven a lanzar) automáticamente a menos que sean capturadas en una sentencia try/catch. Las excepciones a esta regla son send y las funciones de bajo nivel call, delegatecalll y staticcall: devuelven false como su primer valor de retorno en caso de una excepción en lugar de “burbujear”.
A continuación mostramos tanto una llamada de alto nivel como un call de bajo nivel para comparar el comportamiento. Emplearé el método call en el ejemplo siguiente, pero los mismos principios pueden extenderse a delegatecall.
Caller puede llamar a ops() en Called de dos maneras. Ten en cuenta que ops() siempre revierte:
pragma solidity ^0.8.0;
contract Caller {
// first call to ops()
function callByCall(address _address) public returns (bool success) {
(success, ) = _address.call(abi.encodeWithSignature("ops()"));
}
// second call to ops()
function callByInterface(address _address) public {
Called called = Called(_address);
called.ops();
}
}
contract Called {
// ops() always reverts
function ops() public {
revert();
}
}
A pesar de que ambos métodos se utilizan para llamar a la misma función, y ambos métodos usan el opcode CALL, el compilador de solidity genera bytecode para manejar los casos de fallo de diferentes maneras. La ejecución de ambas funciones dentro del contrato Caller revelará que Caller.callByInterface revertirá, mientras que Caller.callByCall no lo hará.
A nivel de la EVM, el opcode CALL devuelve un booleano que indica si la llamada fue exitosa o no, y coloca este retorno en el stack. El opcode en sí no desencadena el revert.
Cuando la llamada se realiza a través de la interfaz del contrato, Solidity maneja este valor de retorno por nosotros. Verifica explícitamente si el valor de retorno es falso e inicia un revert a menos que la llamada se haya realizado dentro de un bloque try/catch.
Sin embargo, al usar llamadas de bajo nivel, necesitamos manejar manualmente este booleano de retorno y desencadenar explícitamente un revert si así lo deseamos.
contract Caller {
//...
function callByCall(address address) public returns (bool success) {
(success, ) = address.call(abi.encodeWithSignature("ops()"));
if (!success) {
revert("Something went wront");
}
}
//...
}
Esta diferencia entre una llamada de alto nivel y una llamada de bajo nivel se ilustra en la siguiente figura.

La diferencia entre call y la llamada por interfaz al llamar a una dirección vacía
El método call de bajo nivel de Solidity no realiza una comprobación previa para verificar si la dirección llamada corresponde a un contrato. El contrato puede comprobar si la dirección es un smart contract usando EXTCODESIZE, que es el opcode tras bambalinas para address.code.length. Si el tamaño es cero, indica que no hay ningún contrato desplegado en esa dirección. Sin embargo, el método call no incorpora esta comprobación; ejecuta directamente el opcode CALL independientemente de ello.
Al usar la interfaz, se comprueba el tamaño del código del objetivo. En otras palabras, en el bytecode generado para la función callByInterface, se ejecuta el opcode EXTCODESIZE en la dirección especificada antes de ejecutar el opcode CALL. Si el tamaño devuelto por EXTCODESIZE es cero, lo que indica que no hay ningún contrato en esa dirección, la función revierte antes de ejecutar el opcode CALL. Esto explica por qué la función callByInterface revierte si se ejecuta con una dirección de contrato inexistente, mientras que callByCall no.
Esta diferencia entre cómo una llamada de bajo nivel y una llamada de alto nivel interactúan con un contrato vacío se ilustra a continuación.

Fundamentalmente, una ejecución puede revertir si encuentra un opcode REVERT, se queda sin gas, o intenta algo prohibido, como dividir por cero. Cuando se realiza una llamada a una dirección vacía, ninguna de las condiciones anteriores puede ocurrir.
Aprende más con RareSkills
Consulta nuestro curso de Solidity gratuito si eres nuevo en Solidity. Si eres un programador de Solidity más experimentado, consulta nuestro Solidity bootcamp avanzado.
Autoría
Este artículo fue escrito por João Paulo Morais en colaboración con RareSkills.
Publicado originalmente el 1 de mayo de 2024