ICRC-2 Swap is 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 mean the design patterns for inter-canister calls are different from other synchronous blockchains.
- 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.
dfx start --clean --background
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)
Deploy Token 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 "'${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;
};
}
})
'
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)
Before we can swap the tokens, they must be transferred to the swap canister. With ICRC-2, this is a two-step process. First we 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 we can call the swap
canister's deposit
method. This method will do the
actual ICRC-1 token transfer, to move the tokens from our wallet into the swap
canister, and then update our deposited token balance in the swap
canister.
Side Note: The amounts we use here are denoted in "e8s". Since out token has 8 decimal places, we writeout 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;
}'
dfx canister call swap swap 'record {
user_a = principal "'${ALICE}'";
user_b = principal "'${BOB}'";
}'
We 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.
After the swap, our balandes in the swap canister will have been updated, and we can withdraw our newly received tokens into our 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;
}'
# 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 "'${ALICE}'";
}'
If everything is working, you should see a your dfx wallet balances reflected in the token balances.
🎉
The example comes with a test suite to demonstrate the basic functionality. It shows how to use this repo from a Javascript client.
dfx start --clean --background
npm install
make test
- Keep a history of deposits/withdrawaps/swaps.
- Add a frontend.
- 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.
Contributions are welcome! Please open an issue or submit a pull request.
- 0xAegir@protonmail.com
- Twitter: @0xAegir