Creating a Decentralized Token Swap on the Internet Computer

·

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:

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:

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:


Project Setup and Prerequisites

Before proceeding, ensure your development environment is ready:

⚠️ 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-swap

This repository contains all necessary source files and configurations.


Explore the Project Structure

Key files include:

The dfx.json file specifies:

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_b

Export 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 balances

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