call, staticcall, and delegatecall are all low-level ways to call functions in another contract. They all allow adjusting the gas limit (compared to transfer and send, which have a fixed limit of 2300 gas) and do not throw exceptions when a call error occurs; instead, they return false. The main differences between these three calling methods are as follows:
- call: Can call functions in other contracts (which can change the state of the called contract, the calling function cannot be view), can be used for transfers (parameter value), and can adjust gas settings.
- delegatecall: Similar to call but cannot be used for transfers (parameter value).
- staticcall: Similar to call but cannot change any state in the contract; thus, delegatecall can only call functions that do not modify state (functions decorated with pure or view). The calling function can be view. In a scenario where a transaction is initiated by an EOA, calling contract A which then calls contract B (EOA -> Contract A -> Contract B), Contract B determines roles using the following table.
Call Type | msg.sender | Context | Update State | Usage |
---|---|---|---|---|
call | A | B | V | Update state (Write) |
delegatecall | A’s msg.sender | A | V | Connect library |
staticcall | A | B | X | Check state (View) |
call
call is a low-level function used to interact with other contracts, and has the following three main usage scenarios:
- Calling functions in other contracts:
<address>.call(bytes memory) returns (bool, bytes memory)
- Similar to send, if the call fails, it will not stop, but will return false.
- Transfer:
<address>.call{value: amount}(bytes memory)
- Similar to transfer, it will transfer amount tokens from the contract to .
- Adjust gas limit:
<address>.call{gas: amount}(bytes memory)
- It will return a bool value.
staticcall
The difference in usage between call and staticcall can be seen through the following code.
delegatecall
In the example above, when user S uses delegatecall through contract A to call contract B, the function executed is from contract B, but the context remains that of contract A (if call is used, the context would be that of contract B). This means that msg.sender is the address of user S, and any changes to State Variables will affect the variables in contract A.
Delegate means to entrust. Through the explanation above, delegatecall can be understood as follows: it is like an investor (user S) entrusting their assets (State Variables of contract A) to an asset management company (contract B) to handle, executing the strategies of the management company (functions of contract B), but changing the state of the assets (variables of contract A).
Delegatecall is commonly used in Proxy Contracts, where smart contracts separate storage contracts from logic contracts:
- The Proxy Contract stores all relevant variables and holds the address of the logic contract.
- All functions are stored in the logic contract, executed through delegatecall. When modifications to the smart contract program are needed, simply redirecting the proxy contract to a new logic contract completes the version update.