sns-launch について
Internet Computer の dapp チーム向けに、SNS DAO の立ち上げ手順を説明するスキルです。NNS 提案と分散化スワップを通じて dapp canister の制御権をコミュニティガバナンスへ移す方法を、設定、テスト、メインネット公開まで扱います。
主な機能
- 01SNS 起動パラメーターの設定
- 02dapp canister の制御権移譲の準備
- 03NNS 提案による SNS 起動
- 04Governance、Ledger、Root、Swap canister のデプロイ
- 05sns-testing によるローカルテスト
- 06メインネットでのテストと公開
活用シーン
- 01デプロイ済み dapp の SNS 公開を計画
- 02SNS ガバナンスとアップグレード手順を検証
- 03NNS 提案と起動設定を準備
- 04スワップ設定やコントローラー構成の問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dfinity/ icskills --skill sns-launchインストール後、Claude Code で/sns-launchを入力して読み込まれたか確認します。
詳細
SNS DAO の立ち上げ
概要
Service Nervous System(SNS)は、個々の Internet Computer dapp を分散化するための DAO フレームワークです。NNS が IC ネットワーク自体を管理するのと同様に、SNS は特定の dapp を管理します。トークン保有者は、コードをアップグレードする提案への投票、トレジャリー資金の管理、パラメーターの設定を行います。SNS を立ち上げると、分散化スワップを通じて、キャニスターの制御が開発者からコミュニティ所有のガバナンスシステムに移ります。
前提条件
- 提案を提出するのに十分なステークを持つ NNS ニューロン(メインネット)
- dapp のキャニスターがすでにメインネットにデプロイされ、動作していること
- すべてのパラメーターを定義した
sns_init.yaml設定ファイル
キャニスター ID
| キャニスター | メインネット ID | 用途 |
|---|---|---|
| NNS Governance | rrkah-fqaaa-aaaaa-aaaaq-cai | SNS 作成提案への投票 |
| SNS-W (Wasm Modules) | qaa6y-5yaaa-aaaaa-aaafa-cai | SNS キャニスターをデプロイして初期化 |
| NNS Root | r7inp-6aaaa-aaaaa-aaabq-cai | 立ち上げ前に dapp の共同コントローラーである必要があります |
| ICP Ledger | ryjl3-tyaaa-aaaaa-aaaba-cai | スワップ中の ICP トークン送金を処理 |
デプロイされる SNS キャニスター
SNS の立ち上げが成功すると、SNS-W は SNS サブネットに以下のキャニスターをデプロイします。
| キャニスター | 用途 |
|---|---|
| Governance | 提案の提出、投票、ニューロン管理 |
| Ledger | SNS トークンの送金(ICRC-1 標準) |
| Root | 立ち上げ後、すべての dapp キャニスターの唯一のコントローラー |
| Swap | 分散化スワップ(ICP と SNS トークンの交換)を実行 |
| Index | SNS Ledger のトランザクションをインデックス化 |
| Archive | 過去のトランザクションを保存 |
ビルドを失敗させるよくある間違い
-
min_participantsを高く設定しすぎる。 500 人の参加者を必要としているのに、実際に参加するのが 200 人だけの場合、スワップ全体が失敗し、ICP はすべて返金されます。最初は控えめに設定しましょう。成功した SNS の立ち上げでは、最低参加者数を 100~200 人に設定するケースがほとんどです。 -
提案の前に NNS Root を共同コントローラーに追加し忘れる。 立ち上げプロセスでは、NNS Root がキャニスターを引き継ぐ必要があります。先に追加せずに提案を提出すると、ステージ 6 で SNS Root が唯一のコントローラーになろうとした際に立ち上げが失敗します。
-
まず SNS テストフライトでテストしない。 すぐにメインネットへ進むと、NNS 提案が公開された後に設定の問題が判明します。ガバナンスとアップグレードのフローを確認するため、必ず最初にメインネットでテストフライト用のモック SNS をデプロイしてください。
-
NNS の審査に通らないトークンエコノミクス。 NNS コミュニティが提案に投票します。妥当でないトークノミクス(開発者への過剰な割り当て、ベスティングなし、非現実的なスワップ上限)は却下されます。コミュニティに受け入れられるパラメーターの範囲を把握するには、成功した SNS の立ち上げ事例(OpenChat、Hot or Not、Kinic)を調べてください。
-
フォールバックコントローラーを定義しない。 スワップが失敗した場合、dapp の制御を戻すコントローラーが必要です。
fallback_controller_principalsを設定しないと、dapp が制御不能になる可能性があります。 -
スワップ期間を短く設定しすぎる。 参加者が各タイムゾーンから参加するには時間が必要です。24 時間未満はリスクが高く、標準的な期間は 3~7 日です。
-
スワップ中に制限される提案タイプを忘れる。 スワップの実行中は、6 種類のガバナンス提案がブロックされます。
ManageNervousSystemParameters、TransferSnsTreasuryFunds、MintSnsTokens、UpgradeSnsControlledCanister、RegisterDappCanisters、DeregisterDappCanistersです。スワップ期間中にこれらを必要とする操作を計画しないでください。 -
開発者ニューロンのディゾルブ遅延がゼロ。 開発者は立ち上げ直後にトークンをすぐ売却できます。長期的なコミットメントを示すため、ディゾルブ遅延とベスティング期間を設定してください(一般的には 12~48 か月です)。
実装
SNS 設定ファイル(sns_init.yaml)
これは、立ち上げパラメーターすべての唯一の信頼できる情報源です。dfinity/sns-testing リポジトリからテンプレートをコピーし、カスタマイズしてください。
# Units: token-valued fields (transaction_fee, rejection_fee, stake, treasury, the swap
# *_icp limits) carry an explicit suffix -- "tokens", "token", or "e8s"
# (1 token = 100_000_000 e8s). Duration fields (initial_voting_period, dissolve_delay,
# vesting_period, ...) carry a time suffix and may be concatenated: seconds, minutes,
# hours, days, weeks, months (30.44 days), years (365.25 days) -- e.g. "4 days", "1w 2d 3h".
# Plain counts such as minimum_participants take no suffix.
# An e8s-only, seconds-only config is the older format; do not mix the two conventions.
# === PROJECT METADATA ===
name: MyProject
description: >
A decentralized application for [purpose].
This proposal requests the NNS to create an SNS for MyProject.
logo: logo.png
url: https://myproject.com
# === NNS PROPOSAL TEXT ===
NnsProposal:
title: "Proposal to create an SNS for MyProject"
url: "https://forum.dfinity.org/t/myproject-sns-proposal/XXXXX"
summary: >
This proposal creates an SNS DAO to govern MyProject.
Token holders will control upgrades, treasury, and parameters.
# === FALLBACK (if swap fails, these principals regain control) ===
fallback_controller_principals:
- YOUR_PRINCIPAL_ID_HERE
# === CANISTER IDS TO DECENTRALIZE ===
dapp_canisters:
- BACKEND_CANISTER_ID
- FRONTEND_CANISTER_ID
# === TOKEN CONFIGURATION ===
Token:
name: MyToken
symbol: MYT
transaction_fee: 0.0001 tokens
logo: token_logo.png
# === GOVERNANCE PARAMETERS ===
Proposals:
rejection_fee: 1 token
initial_voting_period: 4 days
maximum_wait_for_quiet_deadline_extension: 1 day
Neurons:
minimum_creation_stake: 1 token
Voting:
minimum_dissolve_delay: 1 month
MaximumVotingPowerBonuses:
DissolveDelay:
duration: 8 years
bonus: 100% # 2x voting power at max dissolve
Age:
duration: 4 years
bonus: 25%
RewardRate:
initial: 2.5%
final: 2.5%
transition_duration: 0 seconds
# === TOKEN DISTRIBUTION ===
Distribution:
Neurons:
# Developer allocation (with vesting)
- principal: DEVELOPER_PRINCIPAL
stake: 2_000_000 tokens
memo: 0
dissolve_delay: 6 months
vesting_period: 24 months
# Seed investors
- principal: INVESTOR_PRINCIPAL
stake: 500_000 tokens
memo: 1
dissolve_delay: 3 months
vesting_period: 12 months
InitialBalances:
treasury: 5_000_000 tokens # Treasury (controlled by DAO)
swap: 2_500_000 tokens # Sold during decentralization swap
total: 10_000_000 tokens # Must equal sum of all allocations
# === DECENTRALIZATION SWAP ===
Swap:
minimum_participants: 100
minimum_direct_participation_icp: 50_000 tokens
maximum_direct_participation_icp: 500_000 tokens
minimum_participant_icp: 1 token
maximum_participant_icp: 25_000 tokens
duration: 7 days
neurons_fund_participation: true
VestingSchedule:
events: 5 # Neurons unlock in 5 stages
interval: 3 months
confirmation_text: >
I confirm that I am not a resident of a restricted jurisdiction
and I understand the risks of participating in this token swap.
restricted_countries:
- US
- CN
立ち上げプロセス(11 ステージ)
Stage 1: Developer defines parameters in sns_init.yaml
Stage 2: Developer adds NNS Root as co-controller of dapp canisters
Stage 3: Developer submits NNS proposal using `dfx sns propose`
Stage 4: NNS community votes on the proposal
Stage 5: (If adopted) SNS-W deploys uninitialized SNS canisters
Stage 6: SNS Root becomes sole controller of dapp canisters
Stage 7: SNS-W initializes canisters in pre-decentralization-swap mode
Stage 8: 24-hour minimum wait before swap opens
Stage 9: Decentralization swap opens (users send ICP, receive SNS neurons)
Stage 10: Swap closes (time expires or maximum ICP reached)
Stage 11: Finalization (exchange rate set, neurons created, normal mode)
Motoko
キャニスターを SNS による制御に備えてください。重要な要件は、キャニスターが SNS ガバナンスからのアップグレード提案を受け付けることです。
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
persistent actor {
// SNS Root will be set as sole controller after launch.
// Your canister code does not need to change -- SNS governance
// controls upgrades via the standard canister management API.
// If your canister has admin functions, transition them to
// accept SNS governance proposals instead of direct principal checks:
var snsGovernanceId : ?Principal = null;
// ⚠ SECURITY: This setter MUST be access-controlled. Without a check, any caller
// can front-run you and set themselves as governance, permanently locking you out.
// Replace DEPLOYER_PRINCIPAL with your actual principal or use an admin list.
public shared ({ caller }) func setSnsGovernance(id : Principal) : async () {
// Only the deployer (or canister controllers) should call this.
assert (Principal.isController(caller));
switch (snsGovernanceId) {
case (null) { snsGovernanceId := ?id };
case (?_) { Runtime.trap("SNS governance already set") };
};
};
func requireGovernance(caller : Principal) {
switch (snsGovernanceId) {
case (?gov) {
if (caller != gov) { Runtime.trap("Only SNS governance can call this") };
};
case (null) { Runtime.trap("SNS governance not configured") };
};
};
// Admin functions become governance-gated:
public shared ({ caller }) func updateConfig(newFee : Nat) : async () {
requireGovernance(caller);
// ... apply config change
};
};
Rust
use candid::{CandidType, Deserialize, Principal};
use ic_cdk::{init, post_upgrade, query, update};
use std::cell::RefCell;
#[derive(CandidType, Deserialize, Clone)]
struct Config {
sns_governance: Option<Principal>,
}
thread_local! {
// ⚠ STATE LOSS: RefCell<T> in thread_local! is HEAP storage — it is wiped on every
// canister upgrade. In production, use ic-stable-structures (StableCell or StableBTreeMap)
// to persist this across upgrades. At minimum, implement #[pre_upgrade]/#[post_upgrade]
// hooks to serialize/deserialize this data. Without that, an upgrade erases your
// governance config and locks out SNS control.
static CONFIG: RefCell<Config> = RefCell::new(Config {
sns_governance: None,
});
}
fn require_governance(caller: Principal) {
CONFIG.with(|c| {
let config = c.borrow();
match config.sns_governance {
Some(gov) if gov == caller => (),
Some(_) => ic_cdk::trap("Only SNS governance can call this"),
None => ic_cdk::trap("SNS governance not configured"),
}
});
}
// ⚠ SECURITY: This setter MUST be access-controlled. Without a check, any caller
// can front-run you and set themselves as governance, permanently locking you out.
#[update]
fn set_sns_governance(id: Principal) {
// Only canister controllers should call this.
if !ic_cdk::api::is_controller(&ic_cdk::api::msg_caller()) {
ic_cdk::trap("Only canister controllers can set governance");
}
CONFIG.with(|c| {
let mut config = c.borrow_mut();
if config.sns_governance.is_some() {
ic_cdk::trap("SNS governance already set");
}
config.sns_governance = Some(id);
});
}
#[update]
fn update_config(new_fee: u64) {
let caller = ic_cdk::api::msg_caller();
require_governance(caller);
// ... apply config change
}
Cargo.toml の依存関係:
[package]
name = "sns_dapp_backend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
candid = "0.10"
ic-cdk = "0.19"
serde = { version = "1", features = ["derive"] }
デプロイとテスト
sns-testing を使用したローカルテスト
# Clone the SNS testing repository
git clone https://github.com/dfinity/sns-testing.git
cd sns-testing
# WARNING: starting a fresh network wipes all local canister data. Only use for fresh setup.
icp network start -d
# Deploy NNS canisters locally (includes governance, ledger, SNS-W)
# Note: Use the sns-testing repo's setup scripts for NNS + SNS-W canister installation.
# See https://github.com/dfinity/sns-testing for current instructions.
# Deploy your dapp canisters
icp deploy my_backend
icp deploy my_frontend
# Deploy a testflight SNS locally using your config
# Use the sns-testing repo tooling to deploy a local testflight SNS.
# See sns-testing README for the current testflight workflow.
メインネットのテストフライト(Mock SNS)
# Deploy a mock SNS on mainnet to test governance flows
# This does NOT do a real swap -- it creates a mock SNS you control
# Use the sns-testing repo tooling for mainnet testflight deployment.
# See https://github.com/dfinity/sns-testing for the current testflight workflow.
# Test submitting proposals, voting, and upgrading via SNS governance
メインネットへのローンチ(本番)
# Step 1: Add NNS Root as co-controller of each dapp canister
# Requires dfx sns extension: `dfx extension install sns`
dfx sns prepare-canisters add-nns-root BACKEND_CANISTER_ID --network ic
dfx sns prepare-canisters add-nns-root FRONTEND_CANISTER_ID --network ic
# Step 2: Validate your config locally before submitting
dfx sns init-config-file validate
# Or review the rendered proposal by inspecting the yaml output carefully.
# You can also test the full flow on a local replica first (see Local Testing above).
# Step 3: Submit the proposal (THIS IS IRREVERSIBLE — double-check your config)
dfx sns propose --network ic --neuron $NEURON_ID sns_init.yaml
動作確認
ローカルでテストフライトをデプロイした後:
# List deployed SNS canisters
icp canister status sns_governance --id-only
icp canister status sns_ledger --id-only
icp canister status sns_root --id-only
icp canister status sns_swap --id-only
# Verify SNS governance is operational
icp canister call sns_governance get_nervous_system_parameters '()'
# Expected: returns the governance parameters you configured
# Verify token distribution
icp canister call sns_ledger icrc1_total_supply '()'
# Expected: matches your total token supply
# Verify dapp canister controllers changed
icp canister status BACKEND_CANISTER_ID
# Expected: controller is the SNS Root canister, NOT your principal
# Test an SNS proposal (upgrade your canister via governance)
icp canister call sns_governance manage_neuron '(record { ... })'
# Expected: proposal created, can be voted on
メインネットへのローンチ後:
# Check swap status
icp canister call SNS_SWAP_ID get_state '()' -e ic
# Expected: shows swap status, participation count, ICP raised
# Check SNS governance
icp canister call SNS_GOVERNANCE_ID get_nervous_system_parameters '()' -e ic
# Expected: returns your configured parameters
# Verify dapp controller is SNS Root
icp canister status BACKEND_CANISTER_ID -e ic
# Expected: single controller = SNS Root canister ID