Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ninja_pr_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
["Photo Gallery (Rust)"]="rust/photo_gallery"
["Inter-canister calls (Rust)"]="rust/inter-canister-calls"
["X.509 (Rust)"]="rust/x509"
["SNS Kongswap Adaptor (Rust)"]="rust/sns-adaptor"
)

# Check if we should run all examples (workflow file changed) or just changed ones
Expand Down
172 changes: 172 additions & 0 deletions rust/sns-adaptor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# SNS Kongswap Adaptor

[SNS Kongswap Adaptor](https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja) is a Rust-based canister designed to act as an adaptor between the Service Nervous System (SNS) treasury and the KongSwap decentralized exchange on the Internet Computer. Its primary function is to facilitate and automate the management of token assets (such as SNS and ICP tokens) held by a DAO treasury, enabling operations like deposits, withdrawals, balance tracking, and token swaps through KongSwap. The adaptor interacts with multiple canisters, including ledger canisters for different tokens and the KongSwap backend, to execute and audit these operations securely and transparently.

The codebase is structured to ensure robust state management, transaction auditing, and error handling. It provides mechanisms to refresh ledger metadata, manage asset balances, and emit transactions with detailed logging and access control.

Please note that this forked code is slightly modified to ease the interactions. Most drastically:

1. when initializing the adaptor, it doesn't expect any transfers/approvals
2. transfer flow for deposits has changed from `ICRC-2` to `ICRC-1`

## What You Can Learn

This is example teaches you
1. how to build a wrapper around the kongswap adaptor
2. how to interact with [SNS Kongswap Adaptor](https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja).

## Deploying from ICP Ninja

When viewing this project in ICP Ninja, you can deploy it directly to the mainnet for free by clicking "Run" in the upper right corner. Open this project in ICP Ninja:

[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja)

## Local Testing with demo.sh

The `demo.sh` script inside the adaptor's repository provides a complete local testing environment:

## Running the Example

```bash
cd sns-kongswap-adaptor
./demo.sh
```

This will:
1. Start a local IC replica
2. Deploy an SNS and an ICP Ledger
3. Deploy the in-production wasm of Kongswap (as of 9th Septemeber 2025)
4. Deploy the adaptor
5. Deposit to the adaptor

### Prerequisites

1. **Install dfx**: Follow [DFINITY SDK installation](https://internetcomputer.org/docs/current/developer-docs/setup/install/)
2. **Rust toolchain**: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs/ | sh`



### Inspecting Results

After running locally, you can verify the balances:

```bash
(
variant {
Ok = record {
timestamp_ns = 1_757_497_646_192_427_578 : nat64;
asset_to_balances = opt vec {
record {
variant {
Token = record {
ledger_fee_decimals = 10_000 : nat;
ledger_canister_id = principal "ryjl3-tyaaa-aaaaa-aaaba-cai";
symbol = "ICP";
}
};
record {
treasury_owner = opt record {
name = opt "DAO Treasury";
amount_decimals = 0 : nat;
account = opt record {
owner = principal "2vxsx-fae";
subaccount = null;
};
};
suspense = opt record {
name = null;
amount_decimals = 0 : nat;
account = null;
};
fee_collector = opt record {
name = null;
amount_decimals = 20_000 : nat;
account = null;
};
treasury_manager = opt record {
name = opt "KongSwapAdaptor(u6s2n-gx777-77774-qaaba-cai)";
amount_decimals = 0 : nat;
account = opt record {
owner = principal "u6s2n-gx777-77774-qaaba-cai";
subaccount = null;
};
};
external_custodian = opt record {
name = null;
amount_decimals = 80_000 : nat;
account = null;
};
payees = opt record {
name = null;
amount_decimals = 0 : nat;
account = null;
};
payers = opt record {
name = null;
amount_decimals = 0 : nat;
account = null;
};
};
};
record {
variant {
Token = record {
ledger_fee_decimals = 10_000 : nat;
ledger_canister_id = principal "lvfsa-2aaaa-aaaaq-aaeyq-cai";
symbol = "LSNS";
}
};
record {
treasury_owner = opt record {
name = opt "DAO Treasury";
amount_decimals = 0 : nat;
account = opt record {
owner = principal "2vxsx-fae";
subaccount = opt blob "\4d\a0\fd\dd\fd\fb\57\0a\e4\72\d5\e4\07\1c\f5\10\4d\a6\c0\be\71\ca\66\e9\b7\e1\db\6f\6e\ad\1d\c3";
};
};
suspense = opt record {
name = null;
amount_decimals = 0 : nat;
account = null;
};
fee_collector = opt record {
name = null;
amount_decimals = 20_000 : nat;
account = null;
};
treasury_manager = opt record {
name = opt "KongSwapAdaptor(u6s2n-gx777-77774-qaaba-cai)";
amount_decimals = 0 : nat;
account = opt record {
owner = principal "u6s2n-gx777-77774-qaaba-cai";
subaccount = null;
};
};
external_custodian = opt record {
name = null;
amount_decimals = 80_000 : nat;
account = null;
};
payees = opt record {
name = null;
amount_decimals = 0 : nat;
account = null;
};
payers = opt record {
name = null;
amount_decimals = 0 : nat;
account = null;
};
};
};
};
}
},
)
```

This output shows the result of a balance query for two tokens, "ICP" and "LSNS", displaying how each token is distributed among various roles in the treasury system. For both tokens, the balances are split into categories such as `treasury_owner`, `suspense`, `fee_collector`, `treasury_manager`, `external_custodian`, payees, and payers. Most balances are 0, except for `fee_collector` (20_000) and `external_custodian` (80_000).

The values reflect that, when transferring funds to the DEX, two actions each incur a fee of 10_000:
first, giving approval to the DEX, and second, the transfer initiated by the DEX. These fees are accouned in the `fee_collector` (totaling 20_000 per token). The `external_custodian` balance (80_000) is the amount that actually reaches the DEX after fees are deducted. Thus, the output shows the net result of a typical DEX transfer flow, with fees accounted for and the final amount available to the DEX shown under `external_custodian`.
116 changes: 116 additions & 0 deletions rust/sns-adaptor/demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
dfx stop
set -e
trap 'dfx stop' EXIT

echo "Deploying ICP Ledger canister..."
dfx start --background --clean

export MINTER_ACCOUNT_ID=$(dfx --identity anonymous ledger account-id)
export DEFAULT_ACCOUNT_ID=$(dfx ledger account-id)

dfx deploy icp_ledger_canister --argument "
(variant {
Init = record {
minting_account = \"$MINTER_ACCOUNT_ID\";
initial_values = vec {
record {
\"$DEFAULT_ACCOUNT_ID\";
record {
e8s = 10_000_000_000 : nat64;
};
};
};
send_whitelist = vec {};
transfer_fee = opt record {
e8s = 10_000 : nat64;
};
token_symbol = opt \"LICP\";
token_name = opt \"Local ICP\";
}
})
"
dfx canister call icp_ledger_canister account_balance '(record { account = '$(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$DEFAULT_ACCOUNT_ID'")]) + "}")')'})'

echo "Deploying SNS Ledger canister..."
dfx deploy sns_ledger_canister --argument "
(variant {
Init = record {
minting_account = \"$MINTER_ACCOUNT_ID\";
initial_values = vec {
record {
\"$DEFAULT_ACCOUNT_ID\";
record {
e8s = 10_000_000_000 : nat64;
};
};
};
send_whitelist = vec {};
transfer_fee = opt record {
e8s = 10_000 : nat64;
};
token_symbol = opt \"LSNS\";
token_name = opt \"Local SNS\";
}
})
"
dfx canister call sns_ledger_canister account_balance '(record { account = '$(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$DEFAULT_ACCOUNT_ID'")]) + "}")')'})'

echo "Deploying Kong Backend canister..."

dfx deploy kong_backend

export ICP_LEDGER_CANISTER=$(dfx canister id icp_ledger_canister)
export SNS_LEDGER_CANISTER=$(dfx canister id sns_ledger_canister)
export MINTER_PRINCIPAL=$(dfx identity --identity anonymous get-principal)

# We calculate the expected subaccount for the treasury by
# using the function "utils::compute_treasury_subaccount_bytes"
# in kongswap_adaptor/tests/common/utils.rs.
# As principal "lvfsa-2aaaa-aaaaq-aaeyq-cai" is hardcoded in the function,
# we can just run the test to get the expected subaccount bytes.
export TREASURY_SUBACCOUNT="vec{77;160;253;221;253;251;87;10;228;114;213;228;7;28;245;16;77;166;192;190;113;202;102;233;183;225;219;111;110;173;29;195}";

echo "Deploying SNS Kongswap Adaptor canister..."
dfx deploy sns_kongswap_adaptor

TOKENS_TRANSFER_ACCOUNT_ID="$(dfx ledger account-id --of-canister sns_kongswap_adaptor)"
TOKENS_TRANSFER_ACCOUNT_ID_BYTES="$(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$TOKENS_TRANSFER_ACCOUNT_ID'")]) + "}")')"
dfx canister call icp_ledger_canister transfer "(record { to=${TOKENS_TRANSFER_ACCOUNT_ID_BYTES}; amount=record { e8s=100_000 }; fee=record { e8s=10_000 }; memo=0:nat64; }, )"
dfx canister call sns_ledger_canister transfer "(record { to=${TOKENS_TRANSFER_ACCOUNT_ID_BYTES}; amount=record { e8s=100_000 }; fee=record { e8s=10_000 }; memo=0:nat64; }, )"

echo "Balances of the Kongswap Adaptor canister:"
dfx canister call icp_ledger_canister account_balance '(record { account = '$TOKENS_TRANSFER_ACCOUNT_ID_BYTES'})'
dfx canister call sns_ledger_canister account_balance '(record { account = '$TOKENS_TRANSFER_ACCOUNT_ID_BYTES'})'

dfx canister call sns_kongswap_adaptor deposit \
'(record {
allowances = vec {
record {
asset = variant { Token = record {
ledger_fee_decimals = 10000 : nat;
ledger_canister_id = principal "'$SNS_LEDGER_CANISTER'";
symbol = "LSNS";
}};
amount_decimals = 100000 : nat;
owner_account = record {
owner = principal "'$MINTER_PRINCIPAL'";
subaccount = opt '"$TREASURY_SUBACCOUNT"';
};
};
record {
asset = variant { Token = record {
ledger_fee_decimals = 10000 : nat;
ledger_canister_id = principal "'$ICP_LEDGER_CANISTER'";
symbol = "ICP";
}};
amount_decimals = 100000 : nat;
owner_account = record {
owner = principal "'$MINTER_PRINCIPAL'";
subaccount = null;
};
};
};
})'

echo "DONE"
63 changes: 63 additions & 0 deletions rust/sns-adaptor/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"canisters": {
"sns_kongswap_adaptor": {
"candid": "sns-kongswap-adaptor/kongswap_adaptor/kongswap-adaptor.did",
"type": "custom",
"shrink": true,
"gzip": true,
"wasm": "sns-kongswap-adaptor/target/wasm32-unknown-unknown/release/kongswap-adaptor-canister.wasm",
"build": [
"cd sns-kongswap-adaptor && cargo build --target wasm32-unknown-unknown --release -p kongswap_adaptor && cd -"
],
"metadata": [
{
"name": "candid:service"
}
],
"init_arg": "(variant { Init = record { assets = vec { variant { Token = record { ledger_fee_decimals = 10000 : nat; ledger_canister_id = principal \"lvfsa-2aaaa-aaaaq-aaeyq-cai\"; symbol = \"LSNS\"; } }; variant { Token = record { ledger_fee_decimals = 10000 : nat; ledger_canister_id = principal \"ryjl3-tyaaa-aaaaa-aaaba-cai\"; symbol = \"ICP\"; } } } }})"
},
"kong_backend": {
"type": "custom",
"candid": "https://raw.githubusercontent.com/KongSwap/kong/4bf8f99df53dbd34bef0e55ab6364d85bb31c71a/src/kong_backend/kong_backend.did",
"wasm": "https://github.com/KongSwap/kong/raw/4bf8f99df53dbd34bef0e55ab6364d85bb31c71a/wasm/kong_backend.wasm.gz",
"remote": {
"id": {
"ic": "2ipq2-uqaaa-aaaar-qailq-cai"
}
},
"specified_id": "2ipq2-uqaaa-aaaar-qailq-cai"
},
"icp_ledger_canister": {
"type": "custom",
"candid": "https://raw.githubusercontent.com/dfinity/ic/69b755062f5ef0a7d6efc9a127172b46121420c8/rs/ledger_suite/icp/ledger.did",
"wasm": "https://download.dfinity.systems/ic/69b755062f5ef0a7d6efc9a127172b46121420c8/canisters/ledger-canister.wasm.gz",
"remote": {
"id": {
"ic": "ryjl3-tyaaa-aaaaa-aaaba-cai"
}
},
"specified_id": "ryjl3-tyaaa-aaaaa-aaaba-cai",
"init_arg": "(variant { Init = record { minting_account = \"1c7a48ba6a562aa9eaa2481a9049cdf0433b9738c992d698c31d8abf89cadc79\"; initial_values = vec {}; send_whitelist = vec {}; transfer_fee = opt record { e8s = 10_000 : nat64; }; token_symbol = opt \"LICP\"; token_name = opt \"Local ICP\"; } })"
},
"sns_ledger_canister": {
"type": "custom",
"candid": "https://raw.githubusercontent.com/dfinity/ic/83923a194d39835e8a7d9549f9f0831b962a60c2/rs/ledger_suite/icp/ledger.did",
"wasm": "https://download.dfinity.systems/ic/83923a194d39835e8a7d9549f9f0831b962a60c2/canisters/ledger-canister.wasm.gz",
"remote": {
"id": {
"ic": "lvfsa-2aaaa-aaaaq-aaeyq-cai"
}
},
"specified_id": "lvfsa-2aaaa-aaaaq-aaeyq-cai",
"init_arg": "(variant { Init = record { minting_account = \"1c7a48ba6a562aa9eaa2481a9049cdf0433b9738c992d698c31d8abf89cadc79\"; initial_values = vec {}; send_whitelist = vec {}; transfer_fee = opt record { e8s = 10_000 : nat64; }; token_symbol = opt \"LSNS\"; token_name = opt \"Local SNS\"; } })"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
Loading