Four Methods for Inter-Contract Calls in Solidity

·

In medium to large-scale projects, it's impractical to implement all functionality within a single smart contract. Such an approach hinders effective division of labor and collaboration. Typically, code is organized into different libraries or contracts based on functionality, with interfaces provided for mutual interaction.

In Solidity, if the goal is purely code reuse, common code is extracted and deployed as a library. This allows other contracts to use it much like calling a C library or Java library. However, libraries in Solidity cannot define any storage variables, meaning they cannot modify the state of a contract. To alter contract state, you must deploy a new contract, which leads to scenarios where one contract needs to call another.

There are four primary methods for contract-to-contract calls in Solidity:

Understanding CALL and CALLCODE

The fundamental difference between CALL and CALLCODE lies in the context of code execution.

Specifically, CALL modifies the storage of the callee (the contract being called), whereas CALLCODE modifies the storage of the caller (the contract making the call).

To illustrate this, consider the following example contracts:

pragma solidity ^0.4.25;

contract A {
    int public x;

    function inc_call(address _contractAddress) public {
        _contractAddress.call(bytes4(keccak256("inc()")));
    }

    function inc_callcode(address _contractAddress) public {
        _contractAddress.callcode(bytes4(keccak256("inc()")));
    }
}

contract B {
    int public x;

    function inc() public {
        x++;
    }
}

If you call the inc_call() function from contract A and then check the value of x in both contracts, you will find that the value in contract B has increased, while the value in contract A remains unchanged.

Conversely, if you call the inc_callcode() function, the value of x in contract A will be modified, while the value in contract B stays the same. This demonstrates how CALLCODE uses the caller's storage context.

Comparing CALLCODE and DELEGATECALL

DELEGATECALL can be considered a bug-fix version of CALLCODE. The official Solidity documentation now discourages the use of CALLCODE in favor of DELEGATECALL.

The key difference between them concerns the value of msg.sender.

DELEGATECALL preserves the original msg.sender (the address that initiated the transaction), whereas CALLCODE sets msg.sender to the address of the caller contract.

Here is some code to verify this behavior:

pragma solidity ^0.4.25;

contract A {
    int public x;

    function inc_callcode(address _contractAddress) public {
        _contractAddress.callcode(bytes4(keccak256("inc()")));
    }

    function inc_delegatecall(address _contractAddress) public {
        _contractAddress.delegatecall(bytes4(keccak256("inc()")));
    }
}

contract B {
    int public x;
    event senderAddr(address);

    function inc() public {
        x++;
        emit senderAddr(msg.sender);
    }
}

When you call inc_callcode(), the emitted event will show msg.sender as the address of contract A. However, when you call inc_delegatecall(), the event will show msg.sender as the address of the external account that initiated the transaction (the EOA). This preservation of the original caller context is crucial for certain proxy and upgradeability patterns.

The Role of STATICCALL

STATICCALL might seem out of place in this list because, currently, there is no direct low-level API in Solidity to invoke it. Its integration is planned for the future at the compiler level, where calls to view and pure functions will be compiled into STATICCALL instructions.

A view function promises not to modify state variables, and a pure function is even stricter, vowing not to read or modify any state. Presently, this is enforced at compile time. Using STATICCALL would move this guarantee to runtime, meaning a transaction that attempts a state change within a STATICCALL context would simply fail.

The implementation of STATICCALL involves the interpreter setting a readOnly flag to true. Any attempt to perform a write operation while this flag is active results in an errWriteProtection error.

Key Takeaways and Summary

  1. CALL uses the callee's storage context.
  2. CALLCODE and DELEGATECALL use the caller's storage context.
  3. CALL can involve operations between different accounts, like value transfers. CALLCODE and DELEGATECALL act more like libraries, allowing you to use another contract's code and storage within your own contract's context.
  4. The critical difference between CALLCODE and DELEGATECALL is that DELEGATECALL preserves the original msg.sender and msg.value from the top-level call (the EOA). This is particularly useful for creating proxy contracts or implementing upgradeability patterns, as it allows the called contract to operate on behalf of the original user, including making subsequent CALLs for transfers.

Understanding these four methods is essential for Solidity developers working on complex decentralized applications. Each method serves a distinct purpose and enables different patterns of contract interaction and composability 👉 Explore advanced contract interaction strategies.

Frequently Asked Questions

What is the main use case for DELEGATECALL?

DELEGATECALL is primarily used in proxy patterns and upgradeable contract designs. It allows a proxy contract to delegate function execution to a logic contract while maintaining the proxy's storage context and the original msg.sender. This enables the logic of a contract to be changed without migrating the state.

Can CALL be used to transfer Ether to another contract?

Yes, the CALL opcode is the standard way to transfer Ether and execute code in another contract. It allows you to send a specific amount of Wei (the value) to a contract address and optionally trigger a function.

Why is CALLCODE no longer recommended?

CALLCODE is deprecated because it does not correctly preserve the original msg.sender and msg.value. DELEGATECALL was introduced to fix this issue, making it the superior choice for any context where the caller's storage is used and the original transaction details need to be maintained.

What happens if a STATICCALL attempts to modify state?

If a function called via STATICCALL attempts to modify the state, the call will fail at runtime. This provides a runtime guarantee for the behavior promised by view and pure function modifiers, which are currently only checked at compile time.

Is it possible to use DELEGATECALL for all interactions?

No, DELEGATECALL is not suitable for all interactions. It should only be used when the intent is to run the code of another contract within the storage context of the caller. It cannot be used for transferring Ether between contracts in the same way CALL can, as it does not forward value in the same manner.

How do I choose the right call method for my dApp?

The choice depends on your specific needs. Use CALL for independent contract interactions and value transfers. Use DELEGATECALL for proxy patterns and when you need the called contract to act on behalf of the caller's state and identity. Always avoid the deprecated CALLCODE.