Low-Level Calls

All return (bool success, bytes memory returnData) and do not revert on failure — you must check success.

25.1 The three call types

Storage / msg.sender / msg.value context State changes allowed

a's storage; msg.sender = caller contract caller's storage; original msg.sender and msg.value preserved no (reverts on any state change)

All return (bool success, bytes memory returnData) and do not revert on failure — you must check success.

25.2 call

A call to an address with no code succeeds (returns true, empty data). High-level calls check code existence; low-level calls do not. Check target.code.length > 0 when it matters.

Copying huge return data into memory can be used for gas griefing ("returnbomb"); use assembly or libraries (e.g., ExcessivelySafeCall) when calling untrusted contracts.

Prefer typed high-level calls (Target(t).setX(v)) unless you need raw control.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Target {
    uint256 public x;
    function setX(uint256 _x) external payable returns (uint256) { x = _x; return _x * 2; }
}

contract Caller {
    error CallFailed(bytes reason);

    function callSetX(address target, uint256 v) external payable returns (uint256) {
        (bool ok, bytes memory ret) = target.call{value: msg.value, gas: 100_000}(
            abi.encodeCall(Target.setX, (v))
        );
        if (!ok) revert CallFailed(ret);
        return abi.decode(ret, (uint256));
    }

    // Bubble up the original revert reason
    function forward(address target, bytes calldata data) external returns (bytes memory) {
        (bool ok, bytes memory ret) = target.call(data);
        if (!ok) {
            assembly { revert(add(ret, 32), mload(ret)) }
        }
        return ret;
    }
}

25.3 delegatecall

delegatecall runs another contract's code in the context of the caller — the basis of proxies and external libraries.

Dangers: storage layout mismatch corrupts state; delegatecalling untrusted code gives it full control of your storage and balance (it can selfdestruct or overwrite the owner). See Chapter 33.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Logic {
    uint256 public num;        // slot 0
    address public sender;     // slot 1

    function setVars(uint256 _num) external { num = _num; sender = msg.sender; }
}

contract Delegator {
    uint256 public num;        // slot 0 — MUST match Logic's layout
    address public sender;     // slot 1

    function setVars(address logic, uint256 _num) external {
        (bool ok, ) = logic.delegatecall(abi.encodeCall(Logic.setVars, (_num)));
        require(ok, "delegatecall failed");
        // Delegator.num == _num and Delegator.sender == msg.sender; Logic's storage untouched
    }
}

25.4 staticcall

Used for safe read-only queries and to call precompiles (e.g., ecrecover at address(1), sha256 at address(2), modexp at address(5), BN254 pairing at address(8), point evaluation at address(0x0a), BLS12-381 at 0x0b–0x11 after Pectra).

(bool ok, bytes memory ret) = token.staticcall(abi.encodeWithSignature("balanceOf(address)", user));
uint256 bal = ok && ret.length >= 32 ? abi.decode(ret, (uint256)) : 0;

25.5 Multicall pattern

Warning: a delegatecall-based multicall in a payable context lets msg.value be reused in every sub-call — never combine it with functions that trust msg.value (a real exploit class).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract Multicall {
    function multicall(bytes[] calldata data) external returns (bytes[] memory results) {
        results = new bytes[](data.length);
        for (uint256 i; i < data.length; ++i) {
            (bool ok, bytes memory ret) = address(this).delegatecall(data[i]);
            if (!ok) assembly { revert(add(ret, 32), mload(ret)) }
            results[i] = ret;
        }
    }
}