Blog

Understanding Low-Level Call & High-Level Call in Solidity

In Solidity, there are two primary ways to invoke functions or interact with other contracts: high-level and low-level calls. These two methods offer different levels of abstraction and control over the interaction process. The Low-level calls are flexible and provide more control over the execution of the function, while high-level calls are more user-friendly and provide better error handling.


High-Level Calls

A high-level call often referred to as the contract interface, involves using Solidity's built-in function invocation mechanisms, such as directly calling functions on contract instances or addresses. High-level calls provide a more intuitive and abstract way to interact with other contracts, as solidity handles many details for you, such as parameter encoding and decoding, gas estimation, and error handling.

Solidity explicitly verifies whether the return value is false and triggers a revert unless the call was enclosed within a try/catch block.



Behavior

- The compiler checks the function signature and reverts if there's a mismatch.

- The current contract's context (msg.sender, msg.value, etc.) is preserved.

- Gas is automatically transferred and managed.


Low-Level Calls

A low-level call in Solidity refers to a direct invocation of functions on other contracts using functions like call(), delegatecall(), staticcall(), and send(). These functions provide developers with direct access to the Ethereum Virtual Machine (EVM) and offer fine-grained control over contract interactions at the bytecode level.

When making a low-level call, developers manually specify the target contract's address, function signature, and any necessary parameters.



Unlike high-level function calls, where Solidity abstracts away many details, low-level calls require developers to handle parameter encoding, return data decoding, and error checking explicitly.



Behavior

- No compiler checks; errors may occur at runtime.

- The current contract's context is not preserved.

- Gas must be manually managed using `gas()` and `value()`.

Note: `abi.encodeWithSignature` is used to encode the function call data, and `require` is used to check the success of the call.


Insecure low-level call

1. when a low-level call is made using functions like call() or delegatecall(), if the return value is not checked, the execution will continue regardless of whether the called contract throws an exception or reverts. Not checking the return value could introduce security vulnerabilities.

If the called contract encounters an error or reverts its state, but the return value is not checked in the calling contract, the execution in the calling contract continues as if the call was successful. This can lead to unexpected behavior because the calling contract may assume that the call succeeded and proceed with logic based on this assumption.

2. When employing low-level calls in Solidity, it's crucial to recognize that these calls don't inherently verify if the target address corresponds to a deployed smart contract. Low-level calls, such as call() or delegatecall(), directly execute the CALL opcode without confirming the existence of a contract at the specified address.

There are several other issues that can occur when doing low-level calls. It is recommended to exercise with utmost caution when developing/reviewing such implementations.