关于 sns-launch
面向 Internet Computer dapp 团队的 SNS DAO 发布技能,说明如何通过 NNS 提案和去中心化交换,将 dapp canister 的控制权交给社区治理。内容涵盖参数配置、测试与主网上线流程。
主要功能
- 01配置 SNS 启动参数
- 02准备 dapp canister 控制权移交
- 03通过 NNS 提案发起 SNS 发布
- 04部署治理、账本、Root、Swap 等 canister
- 05使用 sns-testing 进行本地测试
- 06在主网测试和执行发布
适用场景
- 01为已部署的 dapp 规划 SNS 上线
- 02验证 SNS 治理和升级流程
- 03准备 NNS 提案及启动配置
- 04排查交换参数和控制器设置问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dfinity/ icskills --skill sns-launch安装后在 Claude Code 中输入/sns-launch确认已加载。
详细介绍
SNS DAO 启动
这是什么
服务神经系统(SNS)是用于实现互联网计算机上各个 dapp 去中心化的 DAO 框架。NNS 管理 IC 网络本身,SNS 则管理特定的 dapp——代币持有者可以对提案投票,以升级代码、管理金库资金并设置参数。启动 SNS 会通过去中心化互换,将 Canister 的控制权从开发者转移到社区拥有的治理系统。
前提条件
- 拥有足够质押量、可提交提案的 NNS 神经元(主网)
- dapp Canister 已部署到主网并正常运行
- 已定义所有参数的
sns_init.yaml配置文件
Canister ID
| Canister | 主网 ID | 用途 |
|---|---|---|
| NNS Governance | rrkah-fqaaa-aaaaa-aaaaq-cai | 对 SNS 创建提案投票 |
| SNS-W (Wasm Modules) | qaa6y-5yaaa-aaaaa-aaafa-cai | 部署并初始化 SNS Canister |
| NNS Root | r7inp-6aaaa-aaaaa-aaabq-cai | 启动前必须成为 dapp 的共同控制器 |
| ICP Ledger | ryjl3-tyaaa-aaaaa-aaaba-cai | 处理互换期间的 ICP 代币转账 |
部署的 SNS Canister
SNS 启动成功后,SNS-W 会在 SNS 子网上部署以下 Canister:
| Canister | 用途 |
|---|---|
| Governance | 提交提案、投票、管理神经元 |
| Ledger | SNS 代币转账(ICRC-1 标准) |
| Root | 启动后作为所有 dapp Canister 的唯一控制器 |
| Swap | 执行去中心化互换(以 ICP 兑换 SNS 代币) |
| Index | 为 SNS Ledger 建立交易索引 |
| Archive | 存储历史交易 |
会导致构建失败的错误
-
将
min_participants设得太高。 如果要求 500 名参与者,但只有 200 人加入,整个互换就会失败,所有 ICP 都会退还。建议从保守的数值开始——大多数成功的 SNS 启动要求至少 100–200 名参与者。 -
提交提案前忘记将 NNS Root 添加为共同控制器。 启动流程要求 NNS Root 接管你的 Canister。如果未先添加就提交提案,SNS Root 在第 6 阶段尝试成为唯一控制器时,启动就会失败。
-
未先在 SNS testflight 上测试。 直接上主网,意味着要等 NNS 提案发布后才发现配置问题。务必先在主网上部署 testflight 模拟 SNS,以验证治理和升级流程。
-
代币经济设计未通过 NNS 审核。 NNS 社区会对你的提案投票。不合理的代币经济设计(开发者分配比例过高、没有归属期、互换上限荒谬)会导致提案被否决。研究成功的 SNS 启动案例(OpenChat、Hot or Not、Kinic),了解社区接受的参数范围。
-
未定义备用控制器。 如果互换失败,dapp 需要有控制器来收回控制权。若没有
fallback_controller_principals,你的 dapp 可能会变得无法控制。 -
互换持续时间设得太短。 不同时区的用户需要时间参与。少于 24 小时有风险——3–7 天是常见设置。
-
忘记在互换期间限制提案类型。 互换进行期间,以下六种治理提案类型会被阻止:
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
让你的 Canister 为接受 SNS 控制做好准备。关键要求是:你的 Canister 必须接受来自 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.
主网 Testflight(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
验证运行情况
本地 Testflight 部署后:
# 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