Solidity has the call function on address data type which can be used to call public and external functions on contracts. It can also be used to transfer ether to addresses.

call is not recommended in most situations for contract function calls because it bypasses type checking, function existence check, and argument packing. It is preferred to import the interface of the contract to call functions on it.

call is used to call the fallback and receive functions of the contract. Receive is called when no data is sent in the function call and ether is sent. Fallback function is called when no function signature matches the call.

call consumes less gas than calling the function on the contract instance. So in some cases call is preferred for gas optimisation.

Solidity has 2 more low level functions delegatecall and staticcall . staticcall is exactly the same as call with only difference that it cannot modify state of the contract being called. delegatecall is discussed below.

How to use call method?

To use call you need to send encoded data as the param. The data will have the function signature and params encoded together.


function myFunction(uint _x, address _addr) public returns(uint, uint) {
    // do something
    return (a, b);
}

// function signature string should not have any spaces
(bool success, bytes memory result) = addr.call(abi.encodeWithSignature("myFunction(uint,address)", 10, msg.sender));

The return value for the call function is a tuple of a boolean and bytes array. The boolean is the success or failure status of the call and the bytes array has the return values of the contract function called which need to be decoded.

To get the value of result in the actual data types you have to use abi.decode

(uint a, uint b) = abi.decode(result, (uint, uint));

decode method takes in the bytes array returned from the function call and the tuple of data types. The returned values are in the same order as the data types tuple.

The call method does not throw any error and the code execution continues. You have to check for success status yourself and do the needful task on success or failure.

If the function passed to the call method does not exist the fallback function of the contract is called. If the fallback function is not implemented in the contract the call method will return success status as false.

How to specify gas and transfer amount

It is possible to supply amount of gas and ether to the call method.

_addr.call{value: 1 ether, gas: 1000000}(abi.encodeWithSignature("myFunction(uint,address)", 10, msg.sender));

To call the myFunction method on the contract only the specified amount of gas will be supplied. The amount of the gas supplied to the outer function where call is executed has to be more or equal to the gas supplied to the call.

If no gas is mentioned all the gas remaining before the call is supplied to the myFunction call. It is always better to not hardcode the gas supplied to the function call.

If you are sending ether to the call method, myFunction should be a payable function. Otherwise the call will fail.

If the amount of gas supplied to call is less than required by myFunction to execute completely the call will fail.

How to send ether using call

After the Istanbul hardfork send and transfer methods have been deprecated. Istanbul hardfork increased the gas cost for SLOAD opcode which is used to read a word from the blockchain. This has been explained in this post by Consensys.

If transfer is used inside a function call of a contract a fixed gas of 2300 is supplied to the transfer method. Since gas cost is subjected to change the fixed gas cost might not be enough to successfully transfer the ether and the overall function call might fail.

When a contract is sent ether using call the receive function is called provided msg.data is empty. If the receive function is not implemented then the fallback function is called.

Using call to transfer ether opens up the possibility of a reentrancy attack since the gas supplied can be used to reenter the function by calling it again inside the receive or fallback function of the receiving contract. Reentrancy can be solved by using a reentrancy guard.

An example of how to use call with reentrancy guard

function paySomeone(address _addr) public payable nonReentrant {
    (bool success, ) = _addr.call{value: msg.value}("");
    // do something
}

call vs delegatecall

delegatecall is used to call a function of contract B from contract A with contract A’s storage, balance and address supplied to the function. This is done for the purpose of using the function in contract B as library code. Since the function will behave as it was a function of Contract A itself. Check this post for a code example.

delegatecall syntax is exactly the same as call syntax except it cannot accept the value option but only gas.

A popular and very useful use case for delegatecall is upgradable contracts. Upgradable contracts use a proxy contract which forwards all the function calls to the implementation contract using delegatecall. The address of the proxy contract remains constant while new implementations can be deployed multiple times. The address of the new implementation gets updated in the proxy contract. The proxy contract has the state of the contract storage intact. Check Openzepplin docs for a detailed explanation.