The Internet Computer (ICP) blockchain empowers developers to build scalable, secure, and fully onchain decentralized finance (DeFi) applications. One of the most fundamental DeFi use cases is a decentralized exchange (DEX) — a platform where users can trade digital assets without relying on centralized intermediaries.
This guide walks you through creating and deploying a decentralized token swap canister that demonstrates core DEX functionality: depositing, swapping, and withdrawing tokens. Built using Motoko and leveraging ICRC-2 compliant tokens, this hands-on tutorial provides a foundation for understanding how trustless asset exchanges work on ICP.
Understanding Decentralized Token Swaps
A decentralized token swap enables peer-to-peer asset exchange directly on the blockchain. Unlike traditional exchanges, there's no central authority holding user funds. Instead, smart contracts — known as canisters on ICP — securely manage token transfers.
In this example, you’ll deploy a simple but functional swap mechanism where two users exchange their respective tokens in a 1:1 ratio. The process involves:
- Depositing tokens into the swap canister
- Swapping one token type for another
- Withdrawing the newly acquired tokens
This implementation uses ICRC-2 tokens, which support the icrc2_approve method for secure, permissioned transfers — a critical feature for decentralized trading.
👉 Discover how blockchain-powered trading is transforming finance today.
Core Components of the Swap Canister
The project consists of three main canisters:
1. swap Canister (Main Logic)
Written in Motoko, this canister handles:
- Accepting deposits via
deposit() - Executing atomic swaps via
swap() - Allowing withdrawals via
withdraw()
It maintains separate balance maps for each token using TrieMap, ensuring efficient lookups and updates.
2. token_a and token_b Canisters (ICRC-2 Tokens)
These are standard-compliant token ledgers deployed using the official ICRC-1 ledger Wasm. Each is initialized with:
- A name and symbol (
Token A/Token B) - An initial balance assigned to
user_aoruser_b - ICRC-2 enabled for advanced transfer control
Project Setup and Prerequisites
Before proceeding, ensure your development environment is ready:
- Install
dfx, the ICP developer toolkit (version 0.15.0 or higher) - Set up a local ICP replica
- Create a project directory (e.g.,
developer_liftoff)
⚠️ This tutorial must be run locally using dfx. Online IDEs like ICP Ninja do not currently support this example.Step-by-Step Implementation
Clone the Example Repository
Open a terminal and run:
git clone https://github.com/dfinity/examples/
cd examples/motoko/icrc2-swapThis repository contains all necessary source files and configurations.
Explore the Project Structure
Key files include:
src/swap/main.mo: Main logic for deposit, swap, and withdraw operationssrc/swap/ICRC.mo: Type definitions for ICRC standardsdfx.json: Configuration defining the three canisters
The dfx.json file specifies:
- The Motoko source for the swap canister
- Remote Wasm and Candid interfaces for the ICRC ledger canisters
This modular setup allows you to focus on business logic while reusing battle-tested token implementations.
Create Identities for Testing
Simulate two users participating in the swap:
dfx identity new user_a
dfx identity new user_bExport their principals as environment variables:
export USER_A=$(dfx identity get-principal --identity user_a)
export USER_B=$(dfx identity get-principal --identity user_b)Set your main identity as the owner:
dfx identity use DevLiftoff
export OWNER=$(dfx identity get-principal)Deploy Token A and Token B
Deploy the first token with an initial balance for user_a:
dfx deploy token_a --argument '
(variant {
Init = record {
token_name = "Token A";
token_symbol = "A";
minting_account = record { owner = principal "'${OWNER}'"; };
initial_balances = vec { record { record { owner = principal "'${USER_A}'"; }; 100_000_000_000; }; };
transfer_fee = 10_000;
feature_flags = opt record { icrc2 = true; };
}
})'Repeat for token_b, assigning the initial balance to user_b:
dfx deploy token_b --argument '
(variant {
Init = record {
token_name = "Token B";
token_symbol = "B";
minting_account = record { owner = principal "'${OWNER}'"; };
initial_balances = vec { record { record { owner = principal "'${USER_B}'"; }; 100_000_000_000; }; };
transfer_fee = 10_000;
feature_flags = opt record { icrc2 = true; };
}
})'Both tokens are now live on your local replica.
Export Canister IDs
Store the canister identifiers for easy reference:
export TOKEN_A=$(dfx canister id token_a)
export TOKEN_B=$(dfx canister id token_b)Deploy the Swap Canister
Now deploy the core swap logic, passing the token canister IDs as initialization arguments:
dfx deploy swap --argument 'record {
token_a = (principal "'${TOKEN_A}'");
token_b = (principal "'${TOKEN_B}'");
}'Save its ID:
export SWAP=$(dfx canister id swap)Deposit Tokens for Swapping
Before swapping, users must approve and deposit their tokens.
Approve Spending
User A approves the swap canister to spend 1.0001 units of Token A (including fee):
dfx canister call --identity user_a token_a icrc2_approve '
record {
amount = 100_010_000;
spender = record { owner = principal "'${SWAP}'"; };
}'User B does the same for Token B.
Execute Deposit
Now deposit exactly 1.0000 units (excluding fee):
dfx canister call --identity user_a swap deposit 'record {
token = principal "'${TOKEN_A}'";
from = record { owner = principal "'${USER_A}'"; };
amount = 100_000_000;
}'Repeat for user_b with token_b.
👉 See how real-world DeFi platforms streamline token management and trading.
Perform the Token Swap
With both deposits complete, execute the atomic swap:
dfx canister call swap swap 'record {
user_a = principal "'${USER_A}'";
user_b = principal "'${USER_B}'";
}'This function transfers all of user_a’s Token A balance to user_b, and vice versa — atomically and without risk of partial execution.
You can verify balances by calling:
dfx canister call swap balancesWithdraw Swapped Tokens
After the swap, users withdraw their new tokens minus the transfer fee.
User A withdraws 99,990,000 units of Token B:
dfx canister call --identity user_a swap withdraw 'record {
token = principal "'${TOKEN_B}'";
to = record { owner = principal "'${USER_A}'"; };
amount = 99_990_000;
}'User B withdraws Token A similarly.
Verify Final Balances
Confirm successful transfers:
dfx canister call token_a icrc1_balance_of 'record { owner = principal "'${USER_A}'"; }'
dfx canister call token_b icrc1_balance_of 'record { owner = principal "'${USER_B}'"; }'Each user should now hold ~998.9998 of their original token and ~1.0 of the new one.
Frequently Asked Questions
Q: What makes this swap "decentralized"?
A: No central party controls funds. All logic runs onchain via smart contracts, and users retain custody until they initiate a transaction.
Q: Why use ICRC-2 instead of ICRC-1?
A: ICRC-2 adds approve and transfer_from, enabling secure third-party spending — essential for DEXs, staking, and lending protocols.
Q: Is the 1:1 swap ratio realistic?
A: This example simplifies pricing for clarity. Real DEXs use automated market makers (AMMs) or order books to determine dynamic rates.
Q: How are upgrade-safe balances preserved?
A: The canister uses stable var storage with preupgrade and postupgrade hooks to serialize TrieMap data during updates.
Q: Can anyone trigger a swap between users?
A: Yes — this version allows any caller to initiate the swap. In production, access controls would restrict this action.
Q: What prevents double-withdrawal attacks?
A: The withdraw function debits the internal balance before initiating the transfer, preventing reentrancy and race conditions.
Key Takeaways
Building a decentralized token swap on ICP showcases the power of onchain computation and secure smart contracts. By combining ICRC standards with Motoko’s safety features, developers can create transparent, tamper-proof financial tools.
Whether you're prototyping a new DEX or integrating swaps into a larger dApp, this pattern provides a solid starting point — fully decentralized, auditable, and extensible.
👉 Start exploring decentralized trading tools built for speed and security.