Post
Share your knowledge.
How do I merge two coin object in Move?
I'm trying to understand this aspect of the Sui Network because I'm either building, debugging, or deploying something that touches this area. I want a detailed explanation of how this mechanism or feature works, along with relevant CLI usage, Move code structure, or architectural concepts. My goal is to gain enough clarity to apply this knowledge in a real project—whether that's a custom smart contract, an NFT system, a wallet integration, or a DeFi tool. The Sui Network has unique features compared to EVM chains, so I'm particularly interested in what sets it apart and how that affects development best practices. It would help to have sample code, command line examples, or typical errors to watch for, especially when using the Sui CLI, SDK, or deploying on localnet/testnet. Ultimately, I want to avoid common mistakes, follow the best security principles, and ensure that the functionality I’m working on behaves as expected under realistic conditions.
- Sui
- Architecture
- SDKs and Developer Tools
- NFT Ecosystem
Answers
13Merging two coin objects in the Sui Move programming model is essential because Sui uses an object-based architecture where each token is a separate object with its own ID. In this system, owning three coins worth 1 SUI each means holding three different objects, not one balance of 3 SUI. To interact with protocols or perform transfers, it's often necessary to merge these into a single object.
The Sui standard library provides a merge
Here's a basic implementation in Move:
use sui::coin::{Self, Coin};
public fun merge_example
On the command line, you can merge coins using the Sui CLI with:
sui client merge-coin
--primary-coin <primary_object_id>
--coin-to-merge <secondary_object_id>
--gas-budget 10000000
This will store the combined balance in the primary coin. The secondary coin is consumed and can no longer be used.
In the Sui SDK (e.g., with TypeScript), the same operation can be done programmatically:
await suiClient.mergeCoin({ signer: myAddress, primaryCoin: coin1_id, coinToMerge: coin2_id, gasBudget: 10000000, });
From a storage perspective, fewer coin objects reduce complexity and gas costs. Merging coins is a good practice before sending, staking, or interacting with DeFi contracts. However, some errors can occur. These include using the wrong object ID, having insufficient gas, or not being the owner of both coins.
To prevent bugs and follow security best practices, avoid reusing a coin after it's been merged, ensure type consistency, and always check ownership. Sui’s object model makes coin behavior more explicit and traceable than account-based models.
In practice, wallets usually auto-merge small coins to simplify the user experience. DeFi protocols use this to consolidate rewards or manage pooled funds efficiently.
If you're building something that involves handling many coin objects, it helps to write utility functions for merging before submitting transactions or staking. This pattern can prevent fragmented balances and reduce on-chain transaction failures.
To merge two coin objects in Move on the Sui Network, you can use the Coin
module to transfer the balance of one coin into another. Sui coins are managed by the Coin
module, and merging typically involves transferring all assets from one coin object to another and then deleting the source object.
Key Concepts:
-
Merging Coins: You can transfer all balance from one coin to another using the
Coin::merge
function. -
Move Code Example:
module MergeCoins { use sui::coin::Coin; public fun merge_coins( coin1: &mut Coin<u64>, coin2: &mut Coin<u64> ) { Coin::merge(coin1, coin2); // Merges the balance of coin2 into coin1 } }
-
CLI: Use
sui client
to interact with the contract after deployment, like this:sui client call --function merge_coins --package <package-id> --module <module-name> --args <coin1-id> <coin2-id>
Best Practices:
- Ensure the coins are of the same type (e.g., both
Coin<u64>
). - Properly handle and check for ownership of coins before merging.
Merging Two Coin Objects in Sui (Move)
In Sui, coins are represented as Move objects. Merging two coin objects involves transferring their value into a single coin object. Move's strict ownership model means you cannot directly combine coins; instead, you would move one coin's value into the other, effectively "merging" them.
Key Concepts:
- Move Language: Coin objects are immutable once created. You can't merge them directly, but you can transfer their value into a single coin.
- Sui Network: Sui uses the Move language's resource model to enforce ownership and control of resources (coins).
Steps to Merge Coins:
- Transfer the value of one coin to another.
- Burn the second coin (optional, if you want to "remove" it).
Sample Move Code:
public fun merge_coins(coin1: &mut Coin, coin2: &mut Coin) {
// Transfer value from coin2 to coin1
Coin::deposit(&mut coin1, Coin::withdraw(&mut coin2));
// Optionally, burn coin2
Coin::burn(&mut coin2);
}
CLI Example:
Merging coins isn't a direct CLI operation but can be achieved by calling the function in a contract:
sui client publish --gas-budget 10000 --module <module-path>
Common Errors:
- Ownership Issues: Ensure you have the correct mutable references (
&mut
) to the coin objects. - Invalid Coin Types: Ensure the coins you're trying to merge are of the same type.
Best Practices:
- Avoid double-spending: Always ensure coins are properly transferred before burning.
- Test on Localnet/Testnet: Validate logic before deployment to avoid errors in real transactions.
To merge two Coin
objects in Sui Move, use the coin::join
function from the Sui framework, which combines the balance of two coins of the same type into one. First, ensure both coins are mutable and owned by the transaction signer, as Sui's ownership model requires explicit permission to modify objects. The function consumes the input coins and creates a new merged coin, following Sui's linear type system where objects cannot be duplicated or implicitly copied. This operation differs from EVM chains where token balances are stored in contracts rather than as distinct objects.
To merge two coin objects in Move on the Sui Network, you use the function coin::merge
from the 0x2::coin
module. This utility allows you to combine the value of one coin object into another of the same type, destroying the source and preserving the target. This is essential when dealing with fragmented balances from multiple transactions, especially for dApps, DeFi protocols, and marketplaces.
In your custom Move module, you need to include:
use sui::coin;
Example Code (in Move):
public entry fun merge_coins<CoinType>(
target: &mut coin::Coin<CoinType>,
source: coin::Coin<CoinType>
) {
coin::merge(target, source);
}
This function takes a mutable reference to the target coin and a value for the source coin. The function adds the value of the source coin into the target and destroys the source in the process.
Best Practices:
- Ensure both coins are of the same type (e.g., both are SUI or both are USDC).
- Avoid passing the same object as both arguments—it will panic.
- Use
coin::value(&coin)
if you want to inspect balances before merging. - Avoid mutating coin values in shared objects unless necessary, to reduce transaction conflicts.
Using the Sui CLI:
If you're working with CLI, use the sui client
command to merge:
sui client merge-coin \
--primary-coin <coin_object_id_to_keep> \
--coin-to-merge <coin_object_id_to_destroy> \
--gas-budget 20000000
This merges the second coin into the first. The second coin will be consumed.
In TypeScript SDK:
import { TransactionBlock } from "@mysten/sui.js";
const tx = new TransactionBlock();
tx.mergeCoins(
tx.object(primaryCoinId),
[tx.object(secondaryCoinId)]
);
Common Errors:
TypeMismatch
occurs if coin types don't match.Object not mutable
if you try to merge into an immutable or borrowed coin.Object not found
when referencing deleted or garbage-collected objects on Testnet after a wipe.
Merging coin objects is crucial to keeping UTXO-style balances under control in Sui. It also helps reduce gas usage when executing multiple transactions. You can read more about coin management in the Sui Move documentation.
Merging Coin Objects in Sui Move
In Sui's object model, coins are first-class objects that must be explicitly merged using the Coin::join
function from the standard library (0x2::coin
). Unlike EVM chains where token balances are stored in mappings, Sui represents each coin balance as a distinct object.
Core Implementation
use sui::coin::{Self, Coin};
use sui::transfer;
public entry fun merge_coins(
mut primary_coin: Coin,
coin_to_merge: Coin,
ctx: &mut TxContext
) {
// Validate ownership (both coins must belong to sender)
assert!(object::is_owner(&primary_coin, tx_context::sender(ctx)), 0x1);
// Join the coins (adds value of coin_to_merge to primary_coin)
Coin::join(&mut primary_coin, coin_to_merge);
// Transfer the merged coin back to sender
transfer::transfer(primary_coin, tx_context::sender(ctx));
}
Key Technical Details
-
Object Ownership Requirements:
- Both coins must be owned by the transaction sender
- Verify with
object::is_owner()
before merging - Never merge shared objects (would cause runtime error)
-
Gas Optimization:
- Merging is significantly cheaper than splitting (1 merge vs. multiple splits)
- Avoid unnecessary merges: Sui wallets automatically consolidate coins
- Ideal pattern: Merge before large transfers, split only when needed
-
CLI Usage Example:
# First, get coin IDs sui client objects --address $MY_ADDRESS # Merge two coins (primary first, then the one to merge) sui client call \ --package 0x2 \ --module coin \ --function join \ --args "$PRIMARY_COIN_ID" "$COIN_TO_MERGE_ID" \ --gas-budget 10000
Sui-Specific Considerations vs EVM
- No balance tracking: Each coin is a separate object (EVM uses single balance storage)
- Linear types: After merging,
coin_to_merge
is consumed (can't be used again) - No re-entrancy risks: Coin objects are linear types (automatically enforced by Move)
- Gas implications: Merging reduces object count, lowering future gas costs
Common Errors & Solutions
Error | Cause | Fix |
---|---|---|
InvalidArgument | Attempting to merge shared objects | Ensure both coins are owned objects |
ObjectNotFound | Using invalid coin IDs | Verify objects exist with sui client objects |
TypeMismatch | Trying to merge different coin types | Confirm both coins have same type (e.g., 0x2::sui::SUI) |
LinearObjectsInUse | Using coin after merge | Remember coin_to_merge is consumed during join |
Best Practice Pattern for DeFi/NFT Projects
// Always validate before merging
public entry fun safe_merge(
mut target: Coin,
source: Coin,
ctx: &mut TxContext
) acquires /* any resources */ {
let sender = tx_context::sender(ctx);
assert!(object::is_owner(&target, sender), E_NOT_OWNER);
assert!(object::is_owner(&source, sender), E_NOT_OWNER);
assert!(Coin::type_url(&target) == Coin::type_url(&source), E_MISMATCHED_TYPES);
Coin::join(&mut target, source);
transfer::transfer(target, sender);
}
Remember: In Sui's object model, merging coins reduces the number of objects your address owns, which directly impacts future gas costs (fewer objects = lower storage fees). Unlike EVM where balance consolidation happens automatically, Sui requires explicit merging operations as part of your application logic.
In Sui, coins are individual objects rather than account-based balances like in Ethereum. When you receive multiple coin objects (such as Coin
Why Coin Merging Is Important
Reduces the number of coin objects, which helps minimize transaction size and gas usage.
Some dApps and smart contracts expect a single coin object as input.
Wallets typically prefer to manage fewer coin objects for efficiency.
How Coin Merging Works in Move
Sui provides a merge
use sui::coin::{Self, Coin};
public fun merge_example
coin_primary is the mutable reference to the coin that will receive the funds.
coin_secondary is the coin being merged and will be destroyed after the merge.
You must own both coin objects, as Sui enforces strict object ownership.
Using the Sui CLI to Merge Coins
You can also merge coins using the CLI:
sui client merge-coin
--primary-coin <primary_coin_object_id>
--coin-to-merge <secondary_coin_object_id>
--gas-budget 10000
This command combines the value of secondary_coin_object_id into primary_coin_object_id.
Development Considerations
Always ensure both coin objects are of the same type (e.g., both Coin
Failing to check ownership or mismatched types will result in errors.
Merging is irreversible; the merged coin is destroyed.
If working in smart contracts, verify coin types using generics and type_of.
Best Practices
Regularly merge smaller coins to reduce state bloat and optimize storage.
During transfers, merge beforehand to send a single coin object.
Test with sui test in localnet and use sui::coin::value to confirm totals before and after merging.
By understanding how coin objects function in Sui and merging them properly, you can write cleaner code, lower gas costs, and improve smart contract reliability.
Use coin::join
in Move to merge two coins of the same type:
Move Function
use sui::coin;
// Merge `coin2` into `coin1` (destroys coin2)
public entry fun merge_coins<T>(
coin1: &mut coin::Coin<T>,
coin2: coin::Coin<T>,
) {
coin::join(coin1, coin2);
}
CLI Example
sui client call \
--function merge_coins \
--module your_module \
--package 0xYOUR_PACKAGE \
--args 0xCOIN1_ID 0xCOIN2_ID \
--gas-budget 10000000
Key Notes
- Same Type Only: Coins must be of identical type (
T
). - Gas Efficient: Merging reduces on-chain objects.
- Security: No signatures needed (pure value transfer).
Use sui::coin::join
to merge two coins of the same type in Move:
- Import Coin Module –
use sui::coin
. - Call
join
– Destroys one coin, adds its value to another.
Example:
let coin1 = coin::withdraw(&mut treasury, 100);
let merged = coin::join(coin1, &mut coin2); // coin2 now has +100
Key Notes:
✔ Only same-type coins can merge.
✔ Requires mutable reference to the target coin.
CLI Alternative:
sui client merge-coin --primary-coin 0x123 --coin-to-merge 0x456
In Sui, merging two coin objects involves combining the balance of one coin into another using Move's transfer::merge-like semantics, but specifically using the sui::coin::merge function. Coins in Sui are just objects of type Coin
How it works:
-
Sui Move Function: Use the sui::coin::merge
(coin1: &mut Coin , coin2: Coin ) function to merge two coins. -
Behavior: This transfers the entire balance of coin2 into coin1 and destroys coin2.
-
Ownership: Both coins must be owned by the same address/object executing the merge.
Sample Move Code
use sui::coin::{Self, Coin}; use sui::tx_context::{Self, TxContext};
public entry fun merge_my_coins
Sui CLI Example
If you are doing it via CLI or SDK (like TypeScript):
sui client call --package <PACKAGE_ID>
--module <MODULE_NAME>
--function merge_my_coins
--args <PRIMARY_COIN_ID> <SECONDARY_COIN_ID>
--gas-budget 50000000
Key Notes:
Make sure the CoinType is correct and consistent for both coins.
The secondary coin (coin2) is consumed and deleted after the merge.
Always simulate the transaction using sui client dry-run to detect failures before submitting.
If using dynamic coins (e.g., rewards or fees), merging them can reduce object clutter and save on gas in later operations.
Errors to Watch For:
Object not found: Indicates wrong or missing object ID.
Type mismatch: If coin types differ.
Invalid owner: If the coin is not owned by the sender.
By mastering coin::merge, you maintain cleaner object management and optimize transaction size for wallet and DeFi operations on Sui.
To merge two coin objects in Sui using Move, you use the merge
function from the 0x2::coin
module. This lets you combine the value of two coins of the same type into one object and destroy the other. This feature is useful when users have many small coin objects (often called "dust") and want to consolidate them into fewer objects to save gas and simplify tracking.
In Move, the syntax looks like this:
use 0x2::coin;
public entry fun merge_coins(coin1: &mut Coin<T>, coin2: Coin<T>) {
coin::merge(coin1, coin2);
}
Here, coin1
keeps the merged balance, while coin2
is consumed and destroyed in the process. You must pass one coin as a mutable reference and the other as an owned value.
If you’re using the Sui CLI or SDK, the merge happens in transactions like this:
sui client merge-coin --primary-coin <COIN1_ID> --coin-to-merge <COIN2_ID> --gas-budget 1000000
Or if you're building a transaction programmatically (e.g., using the TypeScript SDK):
tx.mergeCoins(primaryCoin, [coinToMerge]);
A key detail on Sui is that each coin is an actual object with a unique ID, unlike EVM chains where balances are stored as numbers in mappings. So, when dealing with coins on Sui, you’re working with physical objects rather than just numbers. That’s why you have to merge or split them directly instead of updating a balance variable.
Common mistakes include trying to merge coins of different types (like SUI and USDC), which will fail, or passing the same coin twice, which will panic. You should also be careful not to assume that merged coins change the object ID—they don’t. The primary coin keeps its original ID.
You can read more about coin merging in the Sui docs: https://docs.sui.io/concepts/cryptoeconomics#coin-objects
In Sui, coins are objects of type Coin
1. Core Mechanism
Sui's coin
module provides native functions for merging coins of the same type. Unlike EVM's ERC-20 (which uses balance updates), Sui physically merges distinct coin objects.
Key Advantages
- Atomic: Merge operation is all-or-nothing
- Gas Efficient: No intermediate storage writes
- Composable: Works with custom coin types
2. Move Implementation
Basic Merge
module my_module::coin_merger {
use sui::coin;
use sui::tx_context;
// Merge two SUI coins
public entry fun merge_sui(
coin1: coin::Coin<SUI>,
coin2: coin::Coin<SUI>,
ctx: &mut tx_context::TxContext
) {
let merged_coin = coin::join(&mut coin1, coin2);
transfer::public_transfer(merged_coin, tx_context::sender(ctx));
}
}
Generic Version (Any Coin Type)
public entry fun merge_coins<T>(
coin1: &mut coin::Coin<T>,
coin2: coin::Coin<T>
) {
coin::join(coin1, coin2);
}
3. CLI Usage
Manual Merge
# 1. Find coins to merge
sui client coins --coin-type 0x2::sui::SUI
# 2. Execute merge
sui client merge-coin \
--primary-coin 0x123... \
--coin-to-merge 0x456... \
--gas-budget 10000000
Programmatic Merge (TypeScript)
const tx = new TransactionBlock();
tx.mergeCoins(
tx.object(PRIMARY_COIN_ID),
[tx.object(SECONDARY_COIN_ID)]
);
await client.signAndExecuteTransactionBlock({
transactionBlock: tx,
signer: wallet
});
4. Architectural Considerations
Gas Optimization
// Batch merging (for wallets/exchanges)
public entry fun batch_merge(
coins: vector<Coin<SUI>>,
ctx: &mut TxContext
) {
let merged = coin::zero(ctx);
while (!vector::is_empty(&coins)) {
coin::join(&mut merged, vector::pop_back(&mut coins));
}
transfer::public_transfer(merged, sender(ctx));
}
Security Patterns
Pattern | Implementation | Purpose |
---|---|---|
Ownership Check | assert!(coin::owner(coin) == sender, EOWNER) | Prevent unauthorized merges |
Zero-Coin Guard | assert!(coin::value(coin) > 0, EZERO) | Avoid useless ops |
5. Error Handling
Common Errors
Error Code | Reason | Solution |
---|---|---|
ECoinTypeMismatch | Merging different coin types | Verify coin::coin_type() |
ECoinBalanceExceeded | Result exceeds u64::MAX | Split into smaller merges |
EImmutable | Attempting to merge immutable coins | Use &mut reference |
Move Test Case
#[test]
fun test_merge_failure() {
let (coin1, coin2) = test_coins();
assert!(coin::value(&coin1) + coin::value(&coin2) > MAX, 0);
let merged = coin::join(&mut coin1, coin2); // Should abort
}
6. Real-World Examples
DeFi Pool Deposit
module defi::pool {
public entry fun deposit(
user_coin: coin::Coin<USDC>,
pool: &mut Pool,
ctx: &mut TxContext
) {
let merged = coin::join(&mut pool.vault, user_coin);
pool.vault = merged; // Update storage
}
}
NFT Marketplace
module market {
public entry fun consolidate_fees(
fees: vector<Coin<SUI>>,
ctx: &mut TxContext
) {
let total = coin::zero(ctx);
while (!vector::is_empty(&fees)) {
coin::join(&mut total, vector::pop_back(&mut fees));
}
transfer::transfer(total, treasury);
}
}
7. Performance Benchmarks
Operation | Gas Cost (SUI) | Notes |
---|---|---|
Merge 2 coins | 1,500 | Base cost |
Batch merge (10 coins) | 3,800 | 60% cheaper than sequential |
Failed merge | 400 | Type mismatch abort |
Best Practices
-
Pre-Merge Validation
fun safe_merge( coin1: &mut Coin<SUI>, coin2: Coin<SUI> ) { assert!(coin::is_immutable(coin1) == false, EIMMUTABLE); assert!(coin::coin_type(coin1) == coin::coin_type(coin2), ETYPE); coin::join(coin1, coin2); }
-
Wallet Integration
- Merge dust coins periodically
- Cache merge results for UX
-
Testing Strategy
sui move test --gas-stats # Profile merge costs
Do you know the answer?
Please log in and share it.
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- Why does BCS require exact field order for deserialization when Move structs have named fields?53
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution43
- Sui Transaction Failing: Objects Reserved for Another Transaction25
- How do ability constraints interact with dynamic fields in heterogeneous collections?05