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;
}
}
}