Skip to main content

ICRC-2 swap

View this sample's code on GitHub

Overview

ICRC-2 Swap is a simple canister demonstrating how to safely work with ICRC-2 tokens. It handles depositing, swapping, and withdrawing ICRC-2 tokens.

The asynchronous nature of developing on the Internet Computer presents some unique challenges, which means the design patterns for inter-canister calls are different from other synchronous blockchains.

Features

  • Deposit Tokens: Users can deposit tokens into the contract to be ready for swapping.
  • Swap Tokens: Users can swap the tokens for each other. This is implemented in a very simple naive 1:1 manner. The point is just to demonstrate some minimal behavior.
  • Withdraw Tokens: Users can withdraw the resulting tokens after swapping.

Local deployment

Prerequisites

  • Install the IC SDK.
  • Install Node.js.
  • Clone the example dapp project: git clone https://github.com/dfinity/examples

Step 1: Start a local instance of the replica:

dfx start --clean --background

Step 2: Create your user accounts:

export OWNER=$(dfx identity get-principal)

dfx identity new alice
export ALICE=$(dfx identity get-principal --identity alice)

dfx identity new bob
export BOB=$(dfx identity get-principal --identity bob)

Step 2: Deploy two tokens:

Deploy Token A:

cd examples/motoko/icrc2-swap
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 "'${ALICE}'";
};
100_000_000_000;
};
};
metadata = vec {};
transfer_fee = 10_000;
archive_options = record {
trigger_threshold = 2000;
num_blocks_to_archive = 1000;
controller_id = principal "'${OWNER}'";
};
feature_flags = opt record {
icrc2 = true;
};
}
})
'

Deploy Token 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 "'${BOB}'";
};
100_000_000_000;
};
};
metadata = vec {};
transfer_fee = 10_000;
archive_options = record {
trigger_threshold = 2000;
num_blocks_to_archive = 1000;
controller_id = principal "'${OWNER}'";
};
feature_flags = opt record {
icrc2 = true;
};
}
})
'

Step 3: Deploy the swap canister:

The swap canister accepts deposits and performs the swap.

export TOKEN_A=$(dfx canister id token_a)
export TOKEN_B=$(dfx canister id token_b)

dfx deploy swap --argument '
record {
token_a = (principal "'${TOKEN_A}'");
token_b = (principal "'${TOKEN_B}'");
}
'

export SWAP=$(dfx canister id swap)

Step 4: Approve & deposit tokens:

Before you can swap the tokens, they must be transferred to the swap canister. With ICRC-2, this is a two-step process. First, approve the transfer:

# Approve Bob to deposit 1.00000000 of Token B, and 0.0001 extra for the
# transfer fee
dfx canister call --identity alice token_a icrc2_approve '
record {
amount = 100_010_000;
spender = record {
owner = principal "'${SWAP}'";
};
}
'

# Approve Bob to deposit 1.00000000 of Token B, and 0.0001 extra for the
# transfer fee
dfx canister call --identity bob token_b icrc2_approve '
record {
amount = 100_010_000;
spender = record {
owner = principal "'${SWAP}'";
};
}
'

Then call the swap canister's deposit method. This method will do the actual ICRC-1 token transfer, to move the tokens from your wallet into the swap canister, and then update your deposited token balance in the swap canister.

The amounts you use here are denoted in "e8s". Since your token has 8 decimal places, you write out all 8 decimal places. So 1.00000000 becomes 100,000,000.

# Deposit Alice's tokens
dfx canister call --identity alice swap deposit 'record {
token = principal "'${TOKEN_A}'";
from = record {
owner = principal "'${ALICE}'";
};
amount = 100_000_000;
}'

# Deposit Bob's tokens
dfx canister call --identity bob swap deposit 'record {
token = principal "'${TOKEN_B}'";
from = record {
owner = principal "'${BOB}'";
};
amount = 100_000_000;
}'

Step 5: Perform a swap:

dfx canister call swap swap 'record {
user_a = principal "'${ALICE}'";
user_b = principal "'${BOB}'";
}'

You can check the deposited balances with:

dfx canister call swap balances

That should show us that now Bob holds Token A, and Alice holds Token B in the swap contract.

Step 6: Withdraw tokens:

After the swap, your balances in the swap canister will have been updated, and you can withdraw your newly received tokens into your wallet.

# Withdraw Alice's Token B balance (1.00000000), minus the 0.0001 transfer fee
dfx canister call --identity alice swap withdraw 'record {
token = principal "'${TOKEN_B}'";
to = record {
owner = principal "'${ALICE}'";
};
amount = 99_990_000;
}'
# Withdraw Bob's Token A balance (1.00000000), minus the 0.0001 transfer fee
dfx canister call --identity bob swap withdraw 'record {
token = principal "'${TOKEN_A}'";
to = record {
owner = principal "'${BOB}'";
};
amount = 99_990_000;
}'

Step 7: Check token balances:

# Check Alice's Token A balance. They should now have 998.99980000 A
dfx canister call token_a icrc1_balance_of 'record {
owner = principal "'${ALICE}'";
}'

# Check Bob's Token A balance, They should now have 0.99990000 A.
dfx canister call token_a icrc1_balance_of 'record {
owner = principal "'${BOB}'";
}'

If everything is working, you should see your dfx wallet balances reflected in the token balances.

🎉

Running the test suite

The example comes with a test suite to demonstrate the basic functionality. It shows how to use this repo from a Javascript client.

Prerequisites

Step 1: Start a local instance of the replica:

dfx start --clean --background

Step 2: Install npm dependencies:

npm install

Step 3: Run the test suite:

make test

Possible improvements

  • Keep a history of deposits/withdrawals/swaps.
  • Add a frontend.

Known issues

  • Any DeFi on the Internet Computer is experimental. It is a constantly evolving space, with unknown attacks, and should be treated as such.
  • Due to the nature of asynchronous inter-canister messaging on the IC, it is possible for malicious token canisters to cause this swap contract to deadlock. It should only be used with trusted token canisters.
  • Currently, there are no limits on the state size of this canister. This could allow malicious users to spam the canister, bloating the size until it runs out of space. However, the only way to increase the size is to call deposit, which would cost tokens. For a real canister, you should calculate the maximum size of your canister, limit it to a reasonable amount, and monitor the current size to know when to re-architect.

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Author