Back to all stories
Highlighted Stories
Technical Blogs
Educational

Move for Solidity Developers IV: Cross-Contract Call

8/18/2025
Move for Solidity Developers IV: Cross-Contract Call

“Move for Solidity Developers” is a series created for experienced Solidity developers who already have some knowledge of Move. It helps you shift from writing Solidity contracts to developing on Move-based blockchains, such as Aptos and Sui.

Let’s dig into the concept of cross-contract calls and examine the distinctions between Solidity and Move contracts in this area. We will assess the mechanisms and security of executing cross-contract calls in Move, helping developers better understand how to manage contract interactions within the Move environment.

Understanding Cross-Contract Call

Conceptual Overview

A Cross-Contract Call occurs when one smart contract (Contract A) intentionally executes a function or accesses data residing within a different, separate smart contract (Contract B) during its execution. This interaction happens on-chain via predefined function calls. For instance, in the following code, the Caller contract invokes the setValueAndReceiveEther function on calleeAddress to set a value and simultaneously send ETH.

Code 1

Terminology Alignment

In the Move ecosystem (e.g., Aptos, Sui), the direct equivalent of a Solidity contract is a module. A module bundles related functions and data types. Therefore, a "cross-contract call" is known as a cross-module call in Move. This occurs when a function in one module invokes a public or public(package) function in another. For example, in Move, this might involve calling coin::transfer to send tokens.

Solidity Cross-Contract Calls

Solidity primarily enables interaction with other contracts using higher-level interface-based calls and low-level address operations:

  1. InterfaceName(targetAddress).functionName(arguments): The standard way to interact with known, well-defined external contracts.
  2. address.call{value:amount,gas:gasAmount}(bytes memory payload): The most fundamental and flexible method. Requires manual encoding (abi.encodeWithSignature/Selector) of the function call and decoding of the return data.
  3. address.delegatecall{gas:gasAmount}(bytes memory payload): Primarily used for implementing upgradeable proxy patterns or "library" patterns where reusable logic needs to manipulate the state of the calling contract.
  4. address.staticcall{gas:gasAmount}(bytes memory payload): Reading state or calling pure/view functions on external contracts without risk of state modification.

Move Cross-Module Calls

How To Implement Cross-Module Call

Move fundamentally relies on direct module and function identification for cross-module interaction:

  • use Import: To import a module, you first configure your package's TOML file:

    1. Declare an Alias: In the [addresses] section, assign a readable alias to the on-chain address. (Can be optional if already declared in the dependencies module’s toml file).
    2. Add the Dependency: List the dependencies module in the [dependencies] section.
    3. With the Move.toml file set up, you can import the module with the use keyword, for example, use example::moduleX.

Code 2

  • Direct Function Call: Once a module is imported (or if its path is fully qualified), its public functions are invoked directly using the syntax ModuleName::function_name(arguments). - Example: coin::transfer<USD>(sender,receiver,amount); calls the transfer function in the coin module, specifying the type USDT and passing arguments.
  • Passing References or Objects: Calls typically use immutable (&) or mutable (&mut) references to pass parameters, allowing functions to read or modify resources based on the reference type.
  • No Implicit Caller Concept: Move does not have a global msg.sender to identify the caller of a function. Instead, authorization can be handled explicitly by passing a &signer (in Aptos) or accessing ctx.sender from the transaction context (in Sui).

Below are concrete examples of cross-module calls in Aptos Move and Sui Move. Each example implements a simple Calculator contract calling a Math contract to perform addition.

Aptos Move

Code 3

  1. User call calculator::calculate_sum(user,2,3)
  2. calculator invokes math::add(2,3,user_addr)
  3. math contract uses borrow_global_mut(user_addr) to access user's resource and update the last_result field

Graph

Sui Move

Code 4

  1. User calls calculator::calculate_sum(2,3,user_result)
  2. calculator invokes math::add(2,3,user_result)
  3. math calculates the sum and stores it in user_result

Graph 2

Ultimately, Move re-imagines how smart contracts interact by prioritizing compile-time safety and clarity over runtime flexibility. Unlike Solidity, where contracts communicate through address-based messages that can be dynamic and less predictable, Move requires direct, explicit links between code modules. This fundamental design choice shifts the developer's focus toward a more structured architecture where data permissions and access control are built into the code's foundation. As demonstrated by the different approaches in Aptos and Sui, this model treats interactions not as arbitrary messages but as well-defined function calls between trusted components, leading to a more predictable on-chain environment.

Hidden Traps in Inline/Macro Functions

Both Aptos and Sui Move have implemented inline-like functions, known as inline in Aptos and macro in Sui. During compilation, these functions are expanded at the caller's location. When module A calls an inline function from module B, the compiler directly inserts the function's body at the call site. This technique removes cross-module jumps in the generated bytecode, giving the impression that the execution is happening within the same module.

However, it's important to note that in both Aptos and Sui Move, inline serves only as a compiler optimization hint, copying the function body to the call site without altering the access restrictions on resources or objects.

In the Aptos example below, the sub function needs to access a private resource (UserResult) defined by the math module. However, calling this function from the calculator module will fail because it lacks permission to access the resource (UserResult), which is defined in the math.

Aptos Move

Code 5

Logs:

Logs

In Sui Move, the macro function encounters a similar issue. The sub function in the example below is designed to update the value of the UserResult type, which is defined within the math module. Attempting to call the sub function from a different module will fail because the UserResult type is only accessible within the math module.

Sui Move

Code 7

Logs:

Logs 2

Therefore, while inline functions can offer faster execution and potential gas savings, it is crucial for developers to understand their distinction from regular cross-module calls to prevent incorrect usage. Developers must remember that this feature is a compiler optimization and does not alter Move's fundamental access restrictions, which remain tied to the module where a type is defined.

Cross-Contract Call: Solidity vs Aptos Move vs Sui Move

Solidity's "cross-contract calls" and Move's "cross-module calls" both illustrate how different code units interact within their respective blockchain environments. However, these interactions are governed by distinct philosophies and mechanisms, highlighting key differences in their programming models. Below is a summary table comparing these approaches:

FeatureSolidity Cross-Contract CallAptos Move Cross-Module CallSui Move Cross-Module Call
Storage ModelContract-centric storageAccount-centric storageObject-centric storage
Code Unitscontract keywordmodule keywordmodule keyword
InvocationRequires target address and interface/ABIRequires use statement to import modules related to function callsRequires use statement to import modules related to function calls
Permission Identifiermsg.sender (direct caller)/tx.origin(original initiator of the transaction)signer(function call verifier)tx_context::sender(original initiator of the transaction)
Coin ForwardingExplicit {value: amount} syntax (payable functions)Function call with recipient and amountHandled by passing Coin objects as arguments
Visibilitypublic, external, internal, privatepublic, public(package), package, friend, public(friend), entry, private public, public(package), private, entry, friend(deprecated)

With a solid understanding of cross-module calls, you are now equipped to implement more complex interactions within your Move applications. In the next article, we'll explore the protection mechanism of reentrancy calls in Move.

Elevate Your Web3 Journey
Ready to take the next step? Connect with our sales team to request your free quote and secure your project today!
Client Testimonials