mcpField
S
sns-launch

sns-launch

github.com/dfinity其他39270 次安装今天更新
代码仓库

关于 sns-launch

面向 Internet Computer dapp 团队的 SNS DAO 发布技能,说明如何通过 NNS 提案和去中心化交换,将 dapp canister 的控制权交给社区治理。内容涵盖参数配置、测试与主网上线流程。

主要功能

  1. 01配置 SNS 启动参数
  2. 02准备 dapp canister 控制权移交
  3. 03通过 NNS 提案发起 SNS 发布
  4. 04部署治理、账本、Root、Swap 等 canister
  5. 05使用 sns-testing 进行本地测试
  6. 06在主网测试和执行发布

适用场景

  1. 01为已部署的 dapp 规划 SNS 上线
  2. 02验证 SNS 治理和升级流程
  3. 03准备 NNS 提案及启动配置
  4. 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 Governancerrkah-fqaaa-aaaaa-aaaaq-cai对 SNS 创建提案投票
SNS-W (Wasm Modules)qaa6y-5yaaa-aaaaa-aaafa-cai部署并初始化 SNS Canister
NNS Rootr7inp-6aaaa-aaaaa-aaabq-cai启动前必须成为 dapp 的共同控制器
ICP Ledgerryjl3-tyaaa-aaaaa-aaaba-cai处理互换期间的 ICP 代币转账

部署的 SNS Canister

SNS 启动成功后,SNS-W 会在 SNS 子网上部署以下 Canister:

Canister用途
Governance提交提案、投票、管理神经元
LedgerSNS 代币转账(ICRC-1 标准)
Root启动后作为所有 dapp Canister 的唯一控制器
Swap执行去中心化互换(以 ICP 兑换 SNS 代币)
Index为 SNS Ledger 建立交易索引
Archive存储历史交易

会导致构建失败的错误

  1. 将 min_participants 设得太高。 如果要求 500 名参与者,但只有 200 人加入,整个互换就会失败,所有 ICP 都会退还。建议从保守的数值开始——大多数成功的 SNS 启动要求至少 100–200 名参与者。

  2. 提交提案前忘记将 NNS Root 添加为共同控制器。 启动流程要求 NNS Root 接管你的 Canister。如果未先添加就提交提案,SNS Root 在第 6 阶段尝试成为唯一控制器时,启动就会失败。

  3. 未先在 SNS testflight 上测试。 直接上主网,意味着要等 NNS 提案发布后才发现配置问题。务必先在主网上部署 testflight 模拟 SNS,以验证治理和升级流程。

  4. 代币经济设计未通过 NNS 审核。 NNS 社区会对你的提案投票。不合理的代币经济设计(开发者分配比例过高、没有归属期、互换上限荒谬)会导致提案被否决。研究成功的 SNS 启动案例(OpenChat、Hot or Not、Kinic),了解社区接受的参数范围。

  5. 未定义备用控制器。 如果互换失败,dapp 需要有控制器来收回控制权。若没有 fallback_controller_principals,你的 dapp 可能会变得无法控制。

  6. 互换持续时间设得太短。 不同时区的用户需要时间参与。少于 24 小时有风险——3–7 天是常见设置。

  7. 忘记在互换期间限制提案类型。 互换进行期间,以下六种治理提案类型会被阻止:ManageNervousSystemParameters、TransferSnsTreasuryFunds、MintSnsTokens、UpgradeSnsControlledCanister、RegisterDappCanisters、DeregisterDappCanisters。不要计划在互换期间执行需要这些提案类型的操作。

  8. 开发者神经元的溶解延迟为零。 开发者可以在启动后立即抛售代币。设置溶解延迟和归属期(通常为 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

评论

支持 Markdown