Understanding `.call`, Member Method Calls, `.delegatecall`, `.staticcall` and assembly `call`
\ April 29, 2025 +01:00 \In Solidity, different ways of calling contracts represent different behaviors at the EVM level. Understanding their differences is critical to writing secure and efficient smart contracts.
🤷♂️ TL;DR
.call
: Flexible but dangerous low-level call.- Member call: Safe and easy Solidity wrapper around
.call
. .delegatecall
: Borrow another contract's logic but operate on your own storage..staticcall
: Safe read-only call, guaranteed no state modification.
📜 1. .call
- A low-level EVM instruction.
- Directly calls another contract at a different address.
- Executes using the target contract’s code and the target contract’s own storage.
- Can send ETH along with the call.
- Minimal safety: no type checking, no automatic error handling — you must manually check the return values.
(bool success, bytes memory returnData) = targetAddress.call{value: 0}(data);
✅ Runs with the target's code and the target's storage.
✅ Supports sending ETH.
✅ It's the most fundamental way of cross-contract interaction.
⚡ Major Risk: You must manually check success
, or risk unexpected failures.
📜 2. High-Level Solidity Call (Member Method Call)
When you write:
MyContract(targetAddress).myMethod(args);
- Solidity automatically encodes the arguments into ABI format (
abi.encodeWithSelector(...)
). - Under the hood, it still uses
.call
, but wraps it with:- Automatic
require(success, "Failed")
- Automatic decoding of return values
- Type-safe interfaces
- Automatic
- If the call fails, it reverts automatically with the proper error message.
✅ Safer and easier to write.
⚠️ Slightly more gas-expensive due to extra type checking and error handling.
📜 3. .delegatecall
- Another low-level EVM instruction.
- Executes the target contract’s code but uses the caller’s storage, caller’s balance, and caller’s address.
- The target contract simply provides code; storage and context remain with the caller.
(bool success, bytes memory data) = targetAddress.delegatecall(data);
✅ Commonly used for proxy contracts, plugin systems, and upgradable contracts.
⚡ Very dangerous if misused: a bad call could corrupt your contract's storage layout.
Think of .delegatecall
as "borrowing the target’s code and running it as if it was ME."
📜 4. .staticcall
- Similar to
.call
, but read-only. - Guarantees that no state-changing operations can happen.
- If the target contract tries to modify state, emit events, or send ETH — it will revert.
(bool success, bytes memory data) = targetAddress.staticcall(data);
✅ Used for safely calling view and pure functions across contracts.
✅ Enforces strict immutability: no writes, no events, no state changes.
5. Bonus: Assembly call
Uniswap uses assembly call
to removes the overhead for gas-efficiency.
/// @notice performs a hook call using the given calldata on the given hook that doesn't return a delta
/// @return result The complete data returned by the hook
function callHook(IHooks self, bytes memory data) internal returns (bytes memory result) {
bool success;
assembly ("memory-safe") {
success := call(gas(), self, 0, add(data, 0x20), mload(data), 0, 0)
}
// Revert with FailedHookCall, containing any error message to bubble up
if (!success) CustomRevert.bubbleUpAndRevertWith(address(self), bytes4(data), HookCallFailed.selector);
// The call was successful, fetch the returned data
assembly ("memory-safe") {
// allocate result byte array from the free memory pointer
result := mload(0x40)
// store new free memory pointer at the end of the array padded to 32 bytes
mstore(0x40, add(result, and(add(returndatasize(), 0x3f), not(0x1f))))
// store length in memory
mstore(result, returndatasize())
// copy return data to result
returndatacopy(add(result, 0x20), 0, returndatasize())
}
// Length must be at least 32 to contain the selector. Check expected selector and returned selector match.
if (result.length < 32 || result.parseSelector() != data.parseSelector()) {
InvalidHookResponse.selector.revertWith();
}
}
/// @notice performs a hook call using the given calldata on the given hook
/// @return int256 The delta returned by the hook
function callHookWithReturnDelta(IHooks self, bytes memory data, bool parseReturn) internal returns (int256) {
bytes memory result = callHook(self, data);
// If this hook wasn't meant to return something, default to 0 delta
if (!parseReturn) return 0;
// A length of 64 bytes is required to return a bytes4, and a 32 byte delta
if (result.length != 64) InvalidHookResponse.selector.revertWith();
return result.parseReturnDelta();
}
Source code: Hooks.sol