
“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.
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.

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 primarily enables interaction with other contracts using higher-level interface-based calls and low-level address operations:
InterfaceName(targetAddress).functionName(arguments): The standard way to interact with known, well-defined external contracts.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.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.address.staticcall{gas:gasAmount}(bytes memory payload): Reading state or calling pure/view functions on external contracts without risk of state modification.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:
[addresses] section, assign a readable alias to the on-chain address. (Can be optional if already declared in the dependencies module’s toml file).[dependencies] section.Move.toml file set up, you can import the module with the use keyword, for example, use example::moduleX.
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.immutable (&) or mutable (&mut) references to pass parameters, allowing functions to read or modify resources based on the reference type.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.

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

calculator::calculate_sum(2,3,user_result)calculator invokes math::add(2,3,user_result)math calculates the sum and stores it in user_result
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.
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.


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.


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.
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:
| Feature | Solidity Cross-Contract Call | Aptos Move Cross-Module Call | Sui Move Cross-Module Call |
|---|---|---|---|
| Storage Model | Contract-centric storage | Account-centric storage | Object-centric storage |
| Code Units | contract keyword | module keyword | module keyword |
| Invocation | Requires target address and interface/ABI | Requires use statement to import modules related to function calls | Requires use statement to import modules related to function calls |
| Permission Identifier | msg.sender (direct caller)/tx.origin(original initiator of the transaction) | signer(function call verifier) | tx_context::sender(original initiator of the transaction) |
| Coin Forwarding | Explicit {value: amount} syntax (payable functions) | Function call with recipient and amount | Handled by passing Coin objects as arguments |
| Visibility | public, external, internal, private | public, 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.