MSG Chain dApp KYC/AML 合规接入指南
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
本指南面向 MSG Chain(
msg-chain-1,bech32 前缀msg)上的 dApp 开发者,详细说明如何集成 KYC(了解你的客户)与 AML(反洗钱)合规层。
目录
1. 概述
1.1 为什么 KYC/AML 对 dApp 至关重要
随着全球监管机构对去中心化金融(DeFi)和区块链应用的关注日益增加,合规已成为 dApp 能否持续运营的关键因素。KYC(了解你的客户)和 AML(反洗钱)合规不仅有助于:
- 降低法律风险:避免因违反反洗钱法规而面临的罚款和诉讼
- 保护用户:防止身份盗用和欺诈行为
- 建立信任:吸引机构投资者和合作伙伴
- 可持续发展:为 dApp 与 TradFi(传统金融)的桥梁铺平道路
在 MSG Chain 生态系统中,合规是一个可选层而非强制要求。这使得开发者可以在合规性与去中心化之间取得平衡,根据应用场景选择是否启用合规功能。
1.2 MSG Chain 的合规思路
MSG Chain 采用分层合规架构:
用户 → KYC Provider → 合规验证合约 → dApp 业务合约 → 前端 UI
- KYC Provider:负责身份验证的第三方服务(Civic、Polygon ID、Worldcoin 等)
- 合规验证合约:链上存储合规状态的智能合约(
ComplianceRegistry) - dApp 业务合约:通过查询合规合约来执行访问控制、交易限额等规则
- 前端 UI:向用户展示合规状态,引导完成 KYC 流程
1.3 支持的 KYC Provider
| Provider | 类型 | 隐私级别 | 费用 | 适用场景 |
|---|---|---|---|---|
| Civic | 中心化身份验证 | 低 | 按次付费 | 传统合规 |
| Polygon ID | 零知识证明 | 高 | 免费 | 隐私优先 |
| Worldcoin | 生物特征验证 | 中 | 免费(有限额) | 反 Sybil 攻击 |
| 自定义 Provider | 自建系统 | 可配置 | 自定 | 定制需求 |
1.4 适用场景
- DeFi 协议:借贷、交易所(DEX)需要交易限额
- NFT 市场:合规化铸造和交易
- 稳定币发行:必须遵守 AML 法规
- 现实世界资产(RWA):必须验证投资者身份
- DAO 治理:确保投票者身份真实
1.5 合规层级
定义三个等级的 KYC 身份验证:
| 级别 | 名称 | 所需信息 | 每日交易限额 |
|---|---|---|---|
| 0 | 未认证 | 无 | 无(受限) |
| 1 | 基础认证 | 邮箱 + 手机号 | 1,000 MSG |
| 2 | 高级认证 | 身份证/护照 | 10,000 MSG |
| 3 | 企业认证 | 公司文件 + 法人身份 | 100,000 MSG |
2. 合规架构
2.1 整体架构概览
┌─────────────────────────────────────────────────────────────┐
│ 前端应用 │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ KYC 组件 │ │ 合规状态组件 │ │ dApp 业务界面 │ │
│ └────┬─────┘ └──────────────┘ └──────────────────────┘ │
│ │ │
└───────┼─────────────────────────────────────────────────────┘
│
│ HTTP / WebSocket
│
┌───────┼─────────────────────────────────────────────────────┐
│ │ MSG Chain │
│ ┌────┴─────┐ ┌────────────────┐ ┌────────────────────┐ │
│ │ 合规验证 │◄─│ dApp 业务合约 │ │ 预言机 / IBC │ │
│ │ 合约 │ │ (通过查询集成) │ │ │ │
│ └────┬─────┘ └────────────────┘ └────────────────────┘ │
│ │ │
└───────┼─────────────────────────────────────────────────────┘
│
│ Provider API
│
┌───────┼─────────────────────────────────────────────────────┐
│ ┌────┴─────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ KYC │ │ AML /制裁 │ │ 数据存储(可选) │ │
│ │ Provider │ │ 筛查服务 │ │ │ │
│ └──────────┘ └──────────────┘ └──────────────────────┘ │
│ 第三方服务 │
└─────────────────────────────────────────────────────────────┘
2.2 链上组件
合规验证合约(ComplianceRegistry)
部署在 MSG Chain 上的智能合约,负责:
- 存储用户的 KYC 状态哈希(而非原始身份数据)
- 管理授权验证者(KYC Provider 或管理员)
- 提供查询接口(供其他合约调用)
- 支持撤销验证
dApp 业务合约
调用 ComplianceRegistry 查询接口,在关键操作前执行合规检查。例如:
- 转账前检查发送方是否已通过 KYC
- 根据 KYC 等级设置交易限额
- 暂停不合规用户的操作权限
2.3 链下组件
KYC Provider 服务
处理用户身份验证,包括:
- 收集和验证用户身份信息
- 进行 AML/制裁名单筛查
- 签发经过签名的合规证明(attestation)
- 将证明提交至链上合约
用户数据存储
遵循数据最小化原则,仅在必要时存储用户数据:
- Provider 侧:完整的 KYC 资料(受隐私政策保护)
- 链上:仅存储经过 hash 处理的状态和 Provider 签名
- dApp 侧:一般不存储用户身份数据,仅查询链上状态
2.4 验证流程
用户 KYC Provider MSG Chain (合规合约)
│ │ │
│ 1. 发起 KYC 请求 │ │
│──────────────────────────► │ │
│ │ 2. 验证身份信息 │
│ 提交身份证/护照等 │ │
│──────────────────────────► │ │
│ │ 3. AML 筛查 │
│ │ ──► 检查 PEP/Sanctions │
│ │ │
│ │ 4. 签发签名证明 │
│ │◄────────────────────────── │
│ 5. 获得签名证明 │ │
│◄───────────────────────────│ │
│ │ │
│ 6. 提交证明至合规合约 │ │
│──────────────────────────────────────────────────────► │
│ │ │
│ │ 7. 验证 Provider 签名 │
│ │ 存储合规状态 │
│ │ │
│ 8. KYC 完成,合规状态已更新 │
│◄─────────────────────────────────────────────────────── │
2.5 详细步骤说明
步骤 1:用户发起 KYC 请求
用户在前端点击「开始 KYC 验证」按钮,系统根据配置跳转到相应 Provider 的验证页面。
步骤 2:Provider 验证身份
Provider 使用其身份验证流程核实用户身份:
- Civic:通过其 Secure Scan App 扫描身份证件并进行活体检测
- Polygon ID:用户使用钱包签名并生成零知识证明
- Worldcoin:使用 Orb 设备进行虹膜扫描
步骤 3:AML 筛查
Provider 将用户信息与以下名单交叉比对:
- OFAC(美国外国资产控制办公室)制裁名单
- PEP(政治公众人物)名单
- 各国监管机构黑名单
步骤 4:签发签名证明
KYC 验证通过后,Provider 生成包含以下内容的签名消息:
{
"user_address": "msg1...",
"level": 2,
"expiry": "2026-12-31T23:59:59Z",
"country": "CN",
"provider": "civic",
"nonce": "a1b2c3d4e5"
}
步骤 5:用户获得签名证明
Provider 将签名证明返回给前端,或直接提交至链上合约。
步骤 6-7:提交至链上验证
用户或 Provider 将证明提交至 ComplianceRegistry 合约,合约验证签名合法性并存储合规状态。
步骤 8:KYC 完成
合规状态更新后,dApp 可以通过查询合约获悉用户的 KYC 等级。
2.6 交互模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 链下证明 | Provider 签发离线签名,用户自行提交 | 隐私优先、低成本 |
| 链上直接 | Provider 直接调用合约写入状态 | 高信任度、机构合作 |
| 中继服务 | Provider 通知中继服务代为提交 | 高可用性、简化前端 |
| ZK 证明 | 零知识证明、无需泄露原始数据 | 隐私保护场景 |
3. 合规验证合约
3.1 合约概述
ComplianceRegistry 是 MSG Chain 上的核心合规合约,采用 CosmWasm 框架开发。
部署信息
链 ID: msg-chain-1
合约地址: msg1compliance...(部署后确定)
代码 ID: 待部署
3.2 完整合约代码
use cosmwasm_std::{
entry_point, to_binary, Addr, Binary, Deps, DepsMut, Env,
MessageInfo, Response, StdError, StdResult, Storage,
Timestamp, Uint128,
};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// ──────────────────────────────────────
// 状态定义
// ──────────────────────────────────────
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub admin: Addr,
pub verifier: Addr,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
/// 提交 KYC 验证结果
SubmitKYC {
user: Addr,
level: u8,
expiry: Timestamp,
country: Option<String>,
signature: Binary,
},
/// 撤销用户验证
RevokeKYC {
user: Addr,
},
/// 更新验证者地址
UpdateVerifier {
verifier: Addr,
},
/// 设置等级限额
SetTierLimit {
level: u8,
daily_limit: Uint128,
tx_limit: Uint128,
},
/// 暂停用户
PauseUser {
user: Addr,
},
/// 恢复用户
UnpauseUser {
user: Addr,
},
/// 批量提交
BatchSubmitKYC {
users: Vec<BatchKYCInput>,
},
/// 紧急暂停合约
EmergencyPause {},
/// 恢复合约
EmergencyUnpause {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct BatchKYCInput {
pub user: Addr,
pub level: u8,
pub expiry: Timestamp,
pub country: Option<String>,
pub signature: Binary,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
/// 获取用户状态
GetStatus { user: Addr },
/// 验证用户是否合规
IsCompliant { user: Addr, required_level: u8 },
/// 获取等级限额
GetTierLimit { level: u8 },
/// 获取合约配置
GetConfig {},
/// 查询暂停列表
GetPausedUsers {
start_after: Option<Addr>,
limit: Option<u32>,
},
/// 查询所有已验证用户(分页)
ListVerifiedUsers {
start_after: Option<Addr>,
limit: Option<u32>,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {
pub new_verifier: Option<Addr>,
pub new_admin: Option<Addr>,
}
// ──────────────────────────────────────
// 数据结构
// ──────────────────────────────────────
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct KYCStatus {
/// 验证等级: 0=未验证, 1=基础, 2=高级, 3=企业
pub level: u8,
/// 验证到期时间
pub expiry: Timestamp,
/// 验证者(Provider)地址
pub verifier: Addr,
/// 国家代码(ISO 3166-1 alpha-2)
pub country: Option<String>,
/// 验证时间
pub verified_at: Timestamp,
/// 是否被暂停
pub paused: bool,
/// 当天已使用额度
pub daily_used: Uint128,
/// 上次更新日期
pub last_reset: Timestamp,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TierLimit {
/// 等级
pub level: u8,
/// 每日交易限额
pub daily_limit: Uint128,
/// 单笔交易限额
pub tx_limit: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
/// 管理员地址
pub admin: Addr,
/// 授权验证者(KYC Provider)
pub verifier: Addr,
/// 合约是否暂停
pub paused: bool,
}
// ──────────────────────────────────────
// 存储
// ──────────────────────────────────────
pub const CONFIG: Item<Config> = Item::new("config");
pub const VERIFIED_USERS: Map<&Addr, KYCStatus> = Map::new("verified_users");
pub const TIER_LIMITS: Map<u8, TierLimit> = Map::new("tier_limits");
pub const PAUSED_USERS: Map<&Addr, bool> = Map::new("paused_users");
// ──────────────────────────────────────
// 默认限额
// ──────────────────────────────────────
pub const DEFAULT_TIER_LIMITS: &[(u8, &str, &str)] = &[
(1, "1000", "500"), // 基础: 每日 1000 MSG, 单笔 500 MSG
(2, "10000", "5000"), // 高级: 每日 10000 MSG, 单笔 5000 MSG
(3, "100000", "50000"),// 企业: 每日 100000 MSG, 单笔 50000 MSG
];
// ──────────────────────────────────────
// 错误类型
// ──────────────────────────────────────
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ComplianceError {
Unauthorized,
InvalidSignature,
KYCExpired,
KYCLevelInsufficient { required: u8, current: u8 },
UserPaused,
ContractPaused,
TierLimitExceeded { limit: Uint128, attempted: Uint128 },
DailyLimitExceeded { limit: Uint128, used: Uint128, remaining: Uint128 },
InvalidLevel,
UserNotFound,
AlreadyVerified,
CountryNotAllowed,
BatchSizeExceeded,
}
impl From<ComplianceError> for StdError {
fn from(e: ComplianceError) -> Self {
StdError::generic_err(format!("{:?}", e))
}
}
// ──────────────────────────────────────
// 实例化
// ──────────────────────────────────────
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let config = Config {
admin: msg.admin,
verifier: msg.verifier,
paused: false,
};
CONFIG.save(deps.storage, &config)?;
for &(level, daily, tx) in DEFAULT_TIER_LIMITS {
let limit = TierLimit {
level,
daily_limit: Uint128::from_str(daily).unwrap(),
tx_limit: Uint128::from_str(tx).unwrap(),
};
TIER_LIMITS.save(deps.storage, level, &limit)?;
}
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("admin", msg.admin.to_string())
.add_attribute("verifier", msg.verifier.to_string()))
}
// ──────────────────────────────────────
// 执行入口
// ──────────────────────────────────────
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
let config = CONFIG.load(deps.storage)?;
if config.paused {
return Err(ComplianceError::ContractPaused.into());
}
match msg {
ExecuteMsg::SubmitKYC { user, level, expiry, country, signature } => {
execute_submit_kyc(deps, env, info, config, user, level, expiry, country, signature)
}
ExecuteMsg::RevokeKYC { user } => {
execute_revoke_kyc(deps, env, info, config, user)
}
ExecuteMsg::UpdateVerifier { verifier } => {
execute_update_verifier(deps, env, info, config, verifier)
}
ExecuteMsg::SetTierLimit { level, daily_limit, tx_limit } => {
execute_set_tier_limit(deps, env, info, config, level, daily_limit, tx_limit)
}
ExecuteMsg::PauseUser { user } => {
execute_pause_user(deps, env, info, config, user)
}
ExecuteMsg::UnpauseUser { user } => {
execute_unpause_user(deps, env, info, config, user)
}
ExecuteMsg::BatchSubmitKYC { users } => {
execute_batch_submit_kyc(deps, env, info, config, users)
}
ExecuteMsg::EmergencyPause {} => {
execute_emergency_pause(deps, env, info, config)
}
ExecuteMsg::EmergencyUnpause {} => {
execute_emergency_unpause(deps, env, info, config)
}
}
}
// ──────────────────────────────────────
// 提交 KYC
// ──────────────────────────────────────
fn execute_submit_kyc(
deps: DepsMut,
env: Env,
info: MessageInfo,
config: Config,
user: Addr,
level: u8,
expiry: Timestamp,
country: Option<String>,
signature: Binary,
) -> StdResult<Response> {
if level == 0 || level > 3 {
return Err(ComplianceError::InvalidLevel.into());
}
if expiry <= env.block.time {
return Err(ComplianceError::KYCExpired.into());
}
if info.sender != config.verifier && info.sender != config.admin {
if info.sender != user {
let message = format!("{}{}{}{}", user, level, expiry.nanos(), country.clone().unwrap_or_default());
let hash = deps.api.blake2b_256(message.as_bytes());
deps.api.secp256k1_verify(
&hash,
&signature,
&query_verifier_pubkey(deps.as_ref(), &config.verifier)?,
).map_err(|_| StdError::generic_err("signature verification failed"))?;
} else {
return Err(ComplianceError::Unauthorized.into());
}
}
if let Some(ref c) = country {
if c.len() != 2 {
return Err(ComplianceError::CountryNotAllowed.into());
}
}
if PAUSED_USERS.has(deps.storage, &user) {
return Err(ComplianceError::UserPaused.into());
}
let status = KYCStatus {
level,
expiry,
verifier: config.verifier.clone(),
country,
verified_at: env.block.time,
paused: false,
daily_used: Uint128::zero(),
last_reset: env.block.time,
};
VERIFIED_USERS.save(deps.storage, &user, &status)?;
Ok(Response::new()
.add_attribute("action", "submit_kyc")
.add_attribute("user", user.to_string())
.add_attribute("level", level.to_string())
.add_attribute("expiry", expiry.to_string()))
}
fn execute_revoke_kyc(
deps: DepsMut,
_env: Env,
info: MessageInfo,
config: Config,
user: Addr,
) -> StdResult<Response> {
if info.sender != config.admin && info.sender != config.verifier {
return Err(ComplianceError::Unauthorized.into());
}
VERIFIED_USERS.remove(deps.storage, &user);
Ok(Response::new()
.add_attribute("action", "revoke_kyc")
.add_attribute("user", user.to_string()))
}
fn execute_update_verifier(
deps: DepsMut,
_env: Env,
info: MessageInfo,
mut config: Config,
verifier: Addr,
) -> StdResult<Response> {
if info.sender != config.admin {
return Err(ComplianceError::Unauthorized.into());
}
config.verifier = verifier;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "update_verifier")
.add_attribute("new_verifier", config.verifier.to_string()))
}
fn execute_set_tier_limit(
deps: DepsMut,
_env: Env,
info: MessageInfo,
config: Config,
level: u8,
daily_limit: Uint128,
tx_limit: Uint128,
) -> StdResult<Response> {
if info.sender != config.admin {
return Err(ComplianceError::Unauthorized.into());
}
let limit = TierLimit { level, daily_limit, tx_limit };
TIER_LIMITS.save(deps.storage, level, &limit)?;
Ok(Response::new()
.add_attribute("action", "set_tier_limit")
.add_attribute("level", level.to_string())
.add_attribute("daily_limit", daily_limit.to_string())
.add_attribute("tx_limit", tx_limit.to_string()))
}
fn execute_pause_user(
deps: DepsMut,
_env: Env,
info: MessageInfo,
config: Config,
user: Addr,
) -> StdResult<Response> {
if info.sender != config.admin && info.sender != config.verifier {
return Err(ComplianceError::Unauthorized.into());
}
PAUSED_USERS.save(deps.storage, &user, &true)?;
Ok(Response::new()
.add_attribute("action", "pause_user")
.add_attribute("user", user.to_string()))
}
fn execute_unpause_user(
deps: DepsMut,
_env: Env,
info: MessageInfo,
config: Config,
user: Addr,
) -> StdResult<Response> {
if info.sender != config.admin && info.sender != config.verifier {
return Err(ComplianceError::Unauthorized.into());
}
PAUSED_USERS.remove(deps.storage, &user);
Ok(Response::new()
.add_attribute("action", "unpause_user")
.add_attribute("user", user.to_string()))
}
fn execute_batch_submit_kyc(
deps: DepsMut,
env: Env,
info: MessageInfo,
config: Config,
users: Vec<BatchKYCInput>,
) -> StdResult<Response> {
if info.sender != config.verifier && info.sender != config.admin {
return Err(ComplianceError::Unauthorized.into());
}
const MAX_BATCH: usize = 100;
if users.len() > MAX_BATCH {
return Err(ComplianceError::BatchSizeExceeded.into());
}
let mut count = 0u64;
for input in users {
if input.level == 0 || input.level > 3 { continue; }
if input.expiry <= env.block.time { continue; }
if PAUSED_USERS.has(deps.storage, &input.user) { continue; }
let status = KYCStatus {
level: input.level,
expiry: input.expiry,
verifier: config.verifier.clone(),
country: input.country,
verified_at: env.block.time,
paused: false,
daily_used: Uint128::zero(),
last_reset: env.block.time,
};
VERIFIED_USERS.save(deps.storage, &input.user, &status)?;
count += 1;
}
Ok(Response::new()
.add_attribute("action", "batch_submit_kyc")
.add_attribute("count", count.to_string()))
}
fn execute_emergency_pause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
mut config: Config,
) -> StdResult<Response> {
if info.sender != config.admin {
return Err(ComplianceError::Unauthorized.into());
}
config.paused = true;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "emergency_pause"))
}
fn execute_emergency_unpause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
mut config: Config,
) -> StdResult<Response> {
if info.sender != config.admin {
return Err(ComplianceError::Unauthorized.into());
}
config.paused = false;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "emergency_unpause"))
}
// ──────────────────────────────────────
// 查询入口
// ──────────────────────────────────────
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetStatus { user } => to_binary(&query_status(deps, user)?),
QueryMsg::IsCompliant { user, required_level } => to_binary(&query_is_compliant(deps, user, required_level)?),
QueryMsg::GetTierLimit { level } => to_binary(&query_tier_limit(deps, level)?),
QueryMsg::GetConfig {} => to_binary(&query_config(deps)?),
QueryMsg::GetPausedUsers { start_after, limit } => to_binary(&query_paused_users(deps, start_after, limit)?),
QueryMsg::ListVerifiedUsers { start_after, limit } => to_binary(&query_verified_users(deps, start_after, limit)?),
}
}
fn query_status(deps: Deps, user: Addr) -> StdResult<KYCStatusResponse> {
let status = VERIFIED_USERS
.may_load(deps.storage, &user)?
.unwrap_or(KYCStatus {
level: 0,
expiry: Timestamp::default(),
verifier: Addr::unchecked(""),
country: None,
verified_at: Timestamp::default(),
paused: false,
daily_used: Uint128::zero(),
last_reset: Timestamp::default(),
});
Ok(KYCStatusResponse {
level: status.level,
expiry: status.expiry,
verifier: status.verifier,
country: status.country,
verified_at: status.verified_at,
paused: status.paused,
is_valid: status.level > 0,
})
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct KYCStatusResponse {
pub level: u8,
pub expiry: Timestamp,
pub verifier: Addr,
pub country: Option<String>,
pub verified_at: Timestamp,
pub paused: bool,
pub is_valid: bool,
}
fn query_is_compliant(deps: Deps, user: Addr, required_level: u8) -> StdResult<ComplianceResponse> {
let status = VERIFIED_USERS.may_load(deps.storage, &user)?;
let config = CONFIG.load(deps.storage)?;
if config.paused {
return Ok(ComplianceResponse {
compliant: false,
reason: Some("Contract paused".to_string()),
current_level: 0,
});
}
match status {
Some(s) => {
if s.paused {
return Ok(ComplianceResponse {
compliant: false,
reason: Some("User paused".to_string()),
current_level: s.level,
});
}
if s.level < required_level {
return Ok(ComplianceResponse {
compliant: false,
reason: Some(format!("Required level {}, current {}", required_level, s.level)),
current_level: s.level,
});
}
Ok(ComplianceResponse {
compliant: true,
reason: None,
current_level: s.level,
})
}
None => Ok(ComplianceResponse {
compliant: false,
reason: Some("KYC not completed".to_string()),
current_level: 0,
}),
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ComplianceResponse {
pub compliant: bool,
pub reason: Option<String>,
pub current_level: u8,
}
fn query_tier_limit(deps: Deps, level: u8) -> StdResult<TierLimitResponse> {
let limit = TIER_LIMITS.load(deps.storage, level).unwrap_or(TierLimit {
level,
daily_limit: Uint128::zero(),
tx_limit: Uint128::zero(),
});
Ok(TierLimitResponse {
level: limit.level,
daily_limit: limit.daily_limit,
tx_limit: limit.tx_limit,
})
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TierLimitResponse {
pub level: u8,
pub daily_limit: Uint128,
pub tx_limit: Uint128,
}
fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
let config = CONFIG.load(deps.storage)?;
Ok(ConfigResponse {
admin: config.admin,
verifier: config.verifier,
paused: config.paused,
})
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigResponse {
pub admin: Addr,
pub verifier: Addr,
pub paused: bool,
}
fn query_paused_users(
deps: Deps,
start_after: Option<Addr>,
limit: Option<u32>,
) -> StdResult<Vec<Addr>> {
let limit = limit.unwrap_or(30).min(100) as usize;
PAUSED_USERS
.keys(deps.storage, start_after.as_ref(), None, cosmwasm_std::Order::Ascending)
.take(limit)
.collect()
}
fn query_verified_users(
deps: Deps,
start_after: Option<Addr>,
limit: Option<u32>,
) -> StdResult<Vec<(Addr, KYCStatus)>> {
let limit = limit.unwrap_or(30).min(100) as usize;
VERIFIED_USERS
.range(deps.storage, start_after.as_ref(), None, cosmwasm_std::Order::Ascending)
.take(limit)
.collect::<StdResult<Vec<_>>>()
}
// ──────────────────────────────────────
// 辅助函数
// ──────────────────────────────────────
fn query_verifier_pubkey(deps: Deps, verifier: &Addr) -> StdResult<Binary> {
let pubkey = deps
.storage
.get(b"verifier_pubkey")
.ok_or_else(|| StdError::generic_err("verifier pubkey not found"))?;
Ok(Binary::from(pubkey))
}
// ──────────────────────────────────────
// 单元测试
// ──────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, from_binary, Uint128};
fn setup_contract(deps: DepsMut) {
let msg = InstantiateMsg {
admin: Addr::unchecked("admin"),
verifier: Addr::unchecked("verifier"),
};
let env = mock_env();
let info = mock_info("creator", &[]);
instantiate(deps, env, info, msg).unwrap();
}
#[test]
fn test_instantiate() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let config = CONFIG.load(&deps.storage).unwrap();
assert_eq!(config.admin, Addr::unchecked("admin"));
assert_eq!(config.verifier, Addr::unchecked("verifier"));
assert!(!config.paused);
let tier1 = TIER_LIMITS.load(&deps.storage, 1).unwrap();
assert_eq!(tier1.daily_limit, Uint128::new(1000));
assert_eq!(tier1.tx_limit, Uint128::new(500));
}
#[test]
fn test_submit_kyc() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let info = mock_info("verifier", &[]);
let user = Addr::unchecked("user1");
let msg = ExecuteMsg::SubmitKYC {
user: user.clone(),
level: 2,
expiry: Timestamp::from_nanos(env.block.time.nanos() + 1_000_000_000_000),
country: Some("CN".to_string()),
signature: Binary::default(),
};
execute(deps.as_mut(), env, info, msg).unwrap();
let status = VERIFIED_USERS.load(&deps.storage, &user).unwrap();
assert_eq!(status.level, 2);
assert_eq!(status.country, Some("CN".to_string()));
}
#[test]
fn test_invalid_level() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let info = mock_info("verifier", &[]);
let user = Addr::unchecked("user1");
let msg = ExecuteMsg::SubmitKYC {
user,
level: 5,
expiry: Timestamp::from_nanos(env.block.time.nanos() + 1_000_000_000_000),
country: None,
signature: Binary::default(),
};
let result = execute(deps.as_mut(), env, info, msg);
assert!(result.is_err());
}
#[test]
fn test_unauthorized_submit() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let info = mock_info("hacker", &[]);
let user = Addr::unchecked("user1");
let msg = ExecuteMsg::SubmitKYC {
user,
level: 1,
expiry: Timestamp::from_nanos(env.block.time.nanos() + 1_000_000_000_000),
country: None,
signature: Binary::default(),
};
let result = execute(deps.as_mut(), env, info, msg);
assert!(result.is_err());
}
#[test]
fn test_revoke_kyc() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let verifier_info = mock_info("verifier", &[]);
let admin_info = mock_info("admin", &[]);
let user = Addr::unchecked("user1");
let submit_msg = ExecuteMsg::SubmitKYC {
user: user.clone(),
level: 2,
expiry: Timestamp::from_nanos(env.block.time.nanos() + 1_000_000_000_000),
country: None,
signature: Binary::default(),
};
execute(deps.as_mut(), env.clone(), verifier_info, submit_msg).unwrap();
let revoke_msg = ExecuteMsg::RevokeKYC { user: user.clone() };
execute(deps.as_mut(), env, admin_info, revoke_msg).unwrap();
let status = VERIFIED_USERS.may_load(&deps.storage, &user).unwrap();
assert!(status.is_none());
}
#[test]
fn test_query_status() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let info = mock_info("verifier", &[]);
let user = Addr::unchecked("user1");
let submit_msg = ExecuteMsg::SubmitKYC {
user: user.clone(),
level: 1,
expiry: Timestamp::from_nanos(env.block.time.nanos() + 1_000_000_000_000),
country: Some("JP".to_string()),
signature: Binary::default(),
};
execute(deps.as_mut(), env.clone(), info, submit_msg).unwrap();
let res: KYCStatusResponse = from_binary(
&query(deps.as_ref(), env, QueryMsg::GetStatus { user: user.clone() }).unwrap(),
).unwrap();
assert_eq!(res.level, 1);
assert_eq!(res.country, Some("JP".to_string()));
assert!(res.is_valid);
}
#[test]
fn test_tier_limits() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let admin_info = mock_info("admin", &[]);
let msg = ExecuteMsg::SetTierLimit {
level: 2,
daily_limit: Uint128::new(5000),
tx_limit: Uint128::new(2000),
};
execute(deps.as_mut(), env.clone(), admin_info, msg).unwrap();
let limit = TIER_LIMITS.load(&deps.storage, 2).unwrap();
assert_eq!(limit.daily_limit, Uint128::new(5000));
assert_eq!(limit.tx_limit, Uint128::new(2000));
}
#[test]
fn test_pause_unpause_user() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let admin_info = mock_info("admin", &[]);
let user = Addr::unchecked("user1");
let pause_msg = ExecuteMsg::PauseUser { user: user.clone() };
execute(deps.as_mut(), env.clone(), admin_info.clone(), pause_msg).unwrap();
assert!(PAUSED_USERS.load(&deps.storage, &user).unwrap());
let unpause_msg = ExecuteMsg::UnpauseUser { user: user.clone() };
execute(deps.as_mut(), env, admin_info, unpause_msg).unwrap();
assert!(!PAUSED_USERS.has(&deps.storage, &user));
}
#[test]
fn test_emergency_pause() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let admin_info = mock_info("admin", &[]);
let msg = ExecuteMsg::EmergencyPause {};
execute(deps.as_mut(), env, admin_info, msg).unwrap();
let config = CONFIG.load(&deps.storage).unwrap();
assert!(config.paused);
}
#[test]
fn test_batch_submit() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let info = mock_info("verifier", &[]);
let users = vec![
BatchKYCInput {
user: Addr::unchecked("user1"),
level: 1,
expiry: Timestamp::from_nanos(env.block.time.nanos() + 1_000_000_000_000),
country: None,
signature: Binary::default(),
},
BatchKYCInput {
user: Addr::unchecked("user2"),
level: 2,
expiry: Timestamp::from_nanos(env.block.time.nanos() + 1_000_000_000_000),
country: Some("US".to_string()),
signature: Binary::default(),
},
];
let msg = ExecuteMsg::BatchSubmitKYC { users };
execute(deps.as_mut(), env, info, msg).unwrap();
assert!(VERIFIED_USERS.has(&deps.storage, &Addr::unchecked("user1")));
assert!(VERIFIED_USERS.has(&deps.storage, &Addr::unchecked("user2")));
}
#[test]
fn test_list_verified_users() {
let mut deps = mock_dependencies();
setup_contract(deps.as_mut());
let env = mock_env();
let info = mock_info("verifier", &[]);
for i in 1..=5u8 {
let user = Addr::unchecked(format!("user{}", i));
let submit_msg = ExecuteMsg::SubmitKYC {
user,
level: 1,
expiry: Timestamp::from_nanos(env.block.time.nanos() + 1_000_000_000_000),
country: None,
signature: Binary::default(),
};
execute(deps.as_mut(), env.clone(), info.clone(), submit_msg).unwrap();
}
let res: Vec<(Addr, KYCStatus)> = from_binary(
&query(
deps.as_ref(),
env.clone(),
QueryMsg::ListVerifiedUsers {
start_after: None,
limit: Some(3),
},
).unwrap(),
).unwrap();
assert_eq!(res.len(), 3);
}
}
3.3 测试说明
合约代码包含了完整的单元测试,覆盖以下场景:
| 测试用例 | 测试内容 | 预期结果 |
|---|---|---|
test_instantiate |
合约初始化 | 配置正确,默认限额已设置 |
test_submit_kyc |
提交 KYC 验证 | 状态存储成功,等级和国别正确 |
test_invalid_level |
无效等级 | 返回 InvalidLevel 错误 |
test_unauthorized_submit |
未授权提交 | 返回 Unauthorized 错误 |
test_revoke_kyc |
撤销验证 | 用户状态被移除 |
test_query_status |
查询状态 | 返回正确的等级和国别 |
test_tier_limits |
设置限额 | 限额正确存储 |
test_pause_unpause_user |
暂停/恢复用户 | 状态正确切换 |
test_emergency_pause |
紧急暂停 | 合约进入暂停模式 |
test_batch_submit |
批量提交 | 所有用户成功验证 |
test_list_verified_users |
分页查询 | 返回正确数量 |
运行测试:
cd contracts/compliance-registry
cargo test
cargo wasm
3.4 部署脚本
// deploy.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { GasPrice } from "@cosmjs/stargate";
import * as fs from "fs";
const CHAIN_ID = "msg-chain-1";
const RPC_URL = "https://rpc.msgchain.org";
const DENOM = "umsg";
async function deploy() {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(
"your mnemonic here...",
{ prefix: "msg" }
);
const client = await SigningCosmWasmClient.connectWithSigner(
RPC_URL,
wallet,
{ gasPrice: GasPrice.fromString(`0.025${DENOM}`) }
);
const [account] = await wallet.getAccounts();
console.log(`Deploying from: ${account.address}`);
const wasm = fs.readFileSync(
"target/wasm32-unknown-unknown/release/compliance_registry.wasm"
);
const uploadReceipt = await client.upload(
account.address,
wasm,
"auto"
);
console.log(`Code ID: ${uploadReceipt.codeId}`);
const instantiateMsg = {
admin: "msg1admin...",
verifier: "msg1verifier...",
};
const tx = await client.instantiate(
account.address,
uploadReceipt.codeId,
instantiateMsg,
"ComplianceRegistry",
"auto"
);
console.log(`Contract address: ${tx.contractAddress}`);
return tx.contractAddress;
}
deploy().catch(console.error);
运行部署:
npx ts-node deploy.ts
4. 与 dApp 集成
4.1 集成模式
dApp 可以通过直接查询或跨合约调用两种方式集成合规验证:
方式 A: 直接查询(推荐)
dApp 合约 → 调用 ComplianceRegistry 查询接口
方式 B: 跨合约消息
dApp 合约 → 发送跨合约消息 → ComplianceRegistry → 验证并返回结果
4.2 完整的 DeFi 示例合约
以下是一个完整的 DeFi 借贷协议示例,展示如何集成 KYC 合规验证:
use cosmwasm_std::{
entry_point, to_binary, Addr, Binary, Deps, DepsMut, Env,
MessageInfo, Response, StdError, StdResult, Storage,
Timestamp, Uint128, WasmQuery,
};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// ──────────────────────────────────────
// 合规查询接口
// ──────────────────────────────────────
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ComplianceQuerier {
pub contract_addr: Addr,
}
impl ComplianceQuerier {
pub fn new(addr: Addr) -> Self {
Self { contract_addr: addr }
}
pub fn is_compliant(
&self,
deps: &Deps,
user: &Addr,
required_level: u8,
) -> StdResult<bool> {
let query_msg = to_binary(&QueryMsg::IsCompliant {
user: user.clone(),
required_level,
})?;
let res: ComplianceResponse = deps.querier.query_wasm_smart(
self.contract_addr.clone(),
&query_msg,
)?;
Ok(res.compliant)
}
pub fn get_level(
&self,
deps: &Deps,
user: &Addr,
) -> StdResult<u8> {
let query_msg = to_binary(&QueryMsg::GetStatus {
user: user.clone(),
})?;
let res: KYCStatusResponse = deps.querier.query_wasm_smart(
self.contract_addr.clone(),
&query_msg,
)?;
Ok(res.level)
}
pub fn get_tier_limit(
&self,
deps: &Deps,
level: u8,
) -> StdResult<TierLimitResponse> {
let query_msg = to_binary(&QueryMsg::GetTierLimit { level })?;
let res: TierLimitResponse = deps.querier.query_wasm_smart(
self.contract_addr.clone(),
&query_msg,
)?;
Ok(res)
}
pub fn check_tx_limit(
&self,
deps: &Deps,
user: &Addr,
amount: Uint128,
) -> StdResult<()> {
let level = self.get_level(deps, user)?;
if level == 0 {
return Err(StdError::generic_err("KYC not completed"));
}
let limit = self.get_tier_limit(deps, level)?;
if amount > limit.tx_limit {
return Err(StdError::generic_err(format!(
"Transaction exceeds limit. Max: {}, Attempted: {}",
limit.tx_limit, amount
)));
}
Ok(())
}
}
// ──────────────────────────────────────
// dApp 合约状态
// ──────────────────────────────────────
pub const CONFIG: Item<Config> = Item::new("config");
pub const BALANCES: Map<&Addr, Uint128> = Map::new("balances");
pub const POSITIONS: Map<&Addr, Position> = Map::new("positions");
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub compliance_contract: Addr,
pub admin: Addr,
pub paused: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Position {
pub deposited: Uint128,
pub borrowed: Uint128,
pub collateral: Uint128,
pub last_updated: Timestamp,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub compliance_contract: String,
pub admin: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
Deposit {},
Withdraw { amount: Uint128 },
Borrow { amount: Uint128 },
Repay {},
Liquidate { user: String },
UpdateCompliance { contract: String },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetBalance { user: String },
GetPosition { user: String },
GetConfig {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PositionResponse {
pub deposited: Uint128,
pub borrowed: Uint128,
pub collateral: Uint128,
pub liquidation_price: Uint128,
}
// ──────────────────────────────────────
// 实例化
// ──────────────────────────────────────
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let config = Config {
compliance_contract: deps.api.addr_validate(&msg.compliance_contract)?,
admin: deps.api.addr_validate(&msg.admin)?,
paused: false,
};
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("compliance", msg.compliance_contract))
}
// ──────────────────────────────────────
// 执行入口
// ──────────────────────────────────────
#[entry_point]
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
let config = CONFIG.load(deps.storage)?;
if config.paused {
return Err(StdError::generic_err("Contract paused"));
}
let compliance = ComplianceQuerier::new(config.compliance_contract);
match msg {
ExecuteMsg::Deposit {} => execute_deposit(deps, info, &compliance),
ExecuteMsg::Withdraw { amount } => execute_withdraw(deps, info, amount, &compliance),
ExecuteMsg::Borrow { amount } => execute_borrow(deps, info, amount, &compliance),
ExecuteMsg::Repay {} => execute_repay(deps, info),
ExecuteMsg::Liquidate { user } => execute_liquidate(deps, info, user, &compliance),
ExecuteMsg::UpdateCompliance { contract } => execute_update_compliance(deps, info, contract),
}
}
// ──────────────────────────────────────
// 存款(需 KYC 等级 1)
// ──────────────────────────────────────
fn execute_deposit(
deps: DepsMut,
info: MessageInfo,
compliance: &ComplianceQuerier,
) -> StdResult<Response> {
if !compliance.is_compliant(&deps.as_ref(), &info.sender, 1)? {
return Err(StdError::generic_err("KYC level 1 required for deposits"));
}
let amount = info
.funds
.iter()
.find(|c| c.denom == "umsg")
.map(|c| c.amount)
.unwrap_or(Uint128::zero());
if amount.is_zero() {
return Err(StdError::generic_err("No funds sent"));
}
let mut balance = BALANCES
.may_load(deps.storage, &info.sender)?
.unwrap_or(Uint128::zero());
balance = balance.checked_add(amount)?;
BALANCES.save(deps.storage, &info.sender, &balance)?;
Ok(Response::new()
.add_attribute("action", "deposit")
.add_attribute("user", info.sender.to_string())
.add_attribute("amount", amount.to_string()))
}
// ──────────────────────────────────────
// 提取(需 KYC + 限额检查)
// ──────────────────────────────────────
fn execute_withdraw(
deps: DepsMut,
info: MessageInfo,
amount: Uint128,
compliance: &ComplianceQuerier,
) -> StdResult<Response> {
if !compliance.is_compliant(&deps.as_ref(), &info.sender, 1)? {
return Err(StdError::generic_err("KYC level 1 required for withdrawals"));
}
compliance.check_tx_limit(&deps.as_ref(), &info.sender, amount)?;
let mut balance = BALANCES
.may_load(deps.storage, &info.sender)?
.unwrap_or(Uint128::zero());
if balance < amount {
return Err(StdError::generic_err("Insufficient balance"));
}
balance = balance.checked_sub(amount)?;
BALANCES.save(deps.storage, &info.sender, &balance)?;
Ok(Response::new()
.add_attribute("action", "withdraw")
.add_attribute("user", info.sender.to_string())
.add_attribute("amount", amount.to_string()))
}
// ──────────────────────────────────────
// 借款(需 KYC 等级 2)
// ──────────────────────────────────────
fn execute_borrow(
deps: DepsMut,
info: MessageInfo,
amount: Uint128,
compliance: &ComplianceQuerier,
) -> StdResult<Response> {
if !compliance.is_compliant(&deps.as_ref(), &info.sender, 2)? {
return Err(StdError::generic_err("KYC level 2 required for borrowing"));
}
compliance.check_tx_limit(&deps.as_ref(), &info.sender, amount)?;
let mut position = POSITIONS
.may_load(deps.storage, &info.sender)?
.unwrap_or(Position {
deposited: Uint128::zero(),
borrowed: Uint128::zero(),
collateral: Uint128::zero(),
last_updated: Timestamp::default(),
});
let required_collateral = amount.multiply_ratio(Uint128::from(150u128), Uint128::from(100u128));
if position.collateral < required_collateral {
return Err(StdError::generic_err(format!(
"Insufficient collateral. Required: {}, Available: {}",
required_collateral, position.collateral
)));
}
position.borrowed = position.borrowed.checked_add(amount)?;
position.last_updated = info.block_info().time;
POSITIONS.save(deps.storage, &info.sender, &position)?;
Ok(Response::new()
.add_attribute("action", "borrow")
.add_attribute("user", info.sender.to_string())
.add_attribute("amount", amount.to_string()))
}
// ──────────────────────────────────────
// 还款(无 KYC 要求)
// ──────────────────────────────────────
fn execute_repay(
deps: DepsMut,
info: MessageInfo,
) -> StdResult<Response> {
let payment = info
.funds
.iter()
.find(|c| c.denom == "umsg")
.map(|c| c.amount)
.unwrap_or(Uint128::zero());
let mut position = POSITIONS
.may_load(deps.storage, &info.sender)?
.unwrap_or(Position {
deposited: Uint128::zero(),
borrowed: Uint128::zero(),
collateral: Uint128::zero(),
last_updated: Timestamp::default(),
});
let repay_amount = payment.min(position.borrowed);
position.borrowed = position.borrowed.checked_sub(repay_amount)?;
position.last_updated = info.block_info().time;
POSITIONS.save(deps.storage, &info.sender, &position)?;
Ok(Response::new()
.add_attribute("action", "repay")
.add_attribute("user", info.sender.to_string())
.add_attribute("amount", repay_amount.to_string()))
}
// ──────────────────────────────────────
// 清算(需 KYC 等级 3)
// ──────────────────────────────────────
fn execute_liquidate(
deps: DepsMut,
info: MessageInfo,
user: String,
compliance: &ComplianceQuerier,
) -> StdResult<Response> {
if !compliance.is_compliant(&deps.as_ref(), &info.sender, 3)? {
return Err(StdError::generic_err("KYC level 3 required for liquidation"));
}
let liquidatee = deps.api.addr_validate(&user)?;
let mut position = POSITIONS
.may_load(deps.storage, &liquidatee)?
.ok_or_else(|| StdError::generic_err("Position not found"))?;
let liquidation_amount = position.borrowed;
position.borrowed = Uint128::zero();
position.collateral = Uint128::zero();
POSITIONS.save(deps.storage, &liquidatee, &position)?;
Ok(Response::new()
.add_attribute("action", "liquidate")
.add_attribute("liquidator", info.sender.to_string())
.add_attribute("liquidated_user", user)
.add_attribute("amount", liquidation_amount.to_string()))
}
// ──────────────────────────────────────
// 更新合规合约
// ──────────────────────────────────────
fn execute_update_compliance(
deps: DepsMut,
info: MessageInfo,
contract: String,
) -> StdResult<Response> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(StdError::generic_err("Unauthorized"));
}
config.compliance_contract = deps.api.addr_validate(&contract)?;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "update_compliance")
.add_attribute("new_contract", contract))
}
// ──────────────────────────────────────
// 查询入口
// ──────────────────────────────────────
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetBalance { user } => {
let addr = deps.api.addr_validate(&user)?;
let balance = BALANCES.load(deps.storage, &addr).unwrap_or(Uint128::zero());
to_binary(&balance)
}
QueryMsg::GetPosition { user } => {
let addr = deps.api.addr_validate(&user)?;
let position = POSITIONS.load(deps.storage, &addr)?;
to_binary(&PositionResponse {
deposited: position.deposited,
borrowed: position.borrowed,
collateral: position.collateral,
liquidation_price: position.borrowed.checked_multiply_ratio(
Uint128::from(150u128), Uint128::from(100u128)
).unwrap_or(Uint128::zero()),
})
}
QueryMsg::GetConfig {} => {
let config = CONFIG.load(deps.storage)?;
to_binary(&config)
}
}
}
4.3 前端 SDK
// compliance-sdk.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
const COMPLIANCE_CONTRACT = "msg1compliance...";
const RPC_URL = "https://rpc.msgchain.org";
export interface KYCStatusInfo {
level: number;
expiry: string;
verifier: string;
country: string | null;
verified_at: string;
paused: boolean;
is_valid: boolean;
}
export interface ComplianceResult {
compliant: boolean;
reason: string | null;
current_level: number;
}
export interface TierLimitInfo {
level: number;
daily_limit: string;
tx_limit: string;
}
export class ComplianceSDK {
private client: SigningCosmWasmClient;
private contract: string;
private constructor(client: SigningCosmWasmClient, contract: string) {
this.client = client;
this.contract = contract;
}
static async connect(contract?: string): Promise<ComplianceSDK> {
const client = await SigningCosmWasmClient.connect(RPC_URL);
return new ComplianceSDK(client, contract || COMPLIANCE_CONTRACT);
}
async getStatus(user: string): Promise<KYCStatusInfo> {
return this.client.queryContractSmart(this.contract, {
get_status: { user },
});
}
async isCompliant(
user: string,
requiredLevel: number
): Promise<ComplianceResult> {
return this.client.queryContractSmart(this.contract, {
is_compliant: { user, required_level: requiredLevel },
});
}
async getTierLimit(level: number): Promise<TierLimitInfo> {
return this.client.queryContractSmart(this.contract, {
get_tier_limit: { level },
});
}
async listVerifiedUsers(
startAfter?: string,
limit?: number
): Promise<Array<{ 0: string; 1: KYCStatusInfo }>> {
return this.client.queryContractSmart(this.contract, {
list_verified_users: {
start_after: startAfter,
limit: limit || 30,
},
});
}
async getConfig(): Promise<{
admin: string;
verifier: string;
paused: boolean;
}> {
return this.client.queryContractSmart(this.contract, {
get_config: {},
});
}
}
4.4 集成清单
## dApp 合规集成检查清单
### 合约层
- [ ] 在合约中保存合规合约地址
- [ ] 所有公共函数调用前执行合规检查
- [ ] 按操作类型设置不同的 KYC 等级要求
- [ ] 实现交易限额检查
- [ ] 实现暂停机制(支持管理员暂停不合规用户)
- [ ] 添加紧急暂停功能
- [ ] 实现合规合约地址更新的管理功能
### 后端/API 层
- [ ] 提供查询用户合规状态的 API
- [ ] 缓存合规状态以减少链上查询
- [ ] 监听合规合约的事件并同步状态
- [ ] 实现合规状态变化的通知机制
### 前端层
- [ ] 展示用户的 KYC 状态和等级
- [ ] 引导用户完成 KYC 流程
- [ ] 在操作前检查合规性,提前拦截
- [ ] 显示清晰的错误信息(等级不足、限额超限等)
- [ ] 支持等级升级流程
4.5 交易限额流程图
用户发起交易
│
▼
检查用户 KYC 状态
│
├── 未验证 (level=0) ───────► 拒绝: "请先完成 KYC 验证"
│
├── 已过期 ─────────────────► 拒绝: "KYC 已过期,请重新验证"
│
├── 被暂停 ─────────────────► 拒绝: "账户已被暂停"
│
└── 已验证
│
▼
检查等级是否满足要求
│
├── 等级不足 ───────────► 拒绝: "需要等级 X,当前等级 Y"
│
└── 等级满足
│
▼
检查单笔交易限额
│
├── 超过单笔限额 ───► 拒绝: "单笔交易限额为 X"
│
└── 未超限
│
▼
检查每日累计限额
│
├── 超过每日限额 ───► 拒绝: "每日限额已用尽"
│
└── 未超限
│
▼
执行交易
5. KYC Provider 集成
5.1 Civic 集成
Civic 是一个成熟的去中心化身份验证平台,支持快速 KYC 集成。
工作原理
1. 用户点击 Civic 验证按钮
2. Civic Secure Scan App 扫描身份证件
3. Civic 进行活体检测
4. 验证通过后签发签名证明
5. 证明提交至 MSG Chain 合规合约
前端集成代码
// civic-integration.ts
import { CivicAuth } from "@civic/auth";
export class CivicProvider {
private clientId: string;
private walletAddress: string;
constructor(clientId: string, walletAddress: string) {
this.clientId = clientId;
this.walletAddress = walletAddress;
}
async startKYC(): Promise<CivicResult> {
try {
const auth = new CivicAuth({
clientId: this.clientId,
walletAddress: this.walletAddress,
chain: "cosmos",
});
const result = await auth.authenticate({
scope: ["basic_kyc", "aml_check"],
environment: "production",
});
if (!result.verified) {
return { success: false, error: "Identity verification failed" };
}
const attestation = await auth.getAttestation();
return {
success: true,
attestation: attestation.signature,
level: attestation.level as 1 | 2 | 3,
expiry: attestation.expiry,
country: attestation.country,
};
} catch (error: any) {
return {
success: false,
error: error.message || "Civic KYC failed",
};
}
}
async getStatus(): Promise<{
isVerified: boolean;
level: number;
expiry: string | null;
}> {
const auth = new CivicAuth({
clientId: this.clientId,
walletAddress: this.walletAddress,
chain: "cosmos",
});
const status = await auth.getStatus();
return {
isVerified: status.verified,
level: status.level || 0,
expiry: status.expiry || null,
};
}
}
interface CivicResult {
success: boolean;
error?: string;
attestation?: string;
level?: 1 | 2 | 3;
expiry?: string;
country?: string;
}
Civic 回调处理
// civic-callback.ts
export async function handleCivicCallback(
code: string,
userAddress: string,
signer: SigningCosmWasmClient
) {
const tokenResponse = await fetch("https://api.civic.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
code,
client_id: process.env.CIVIC_CLIENT_ID,
client_secret: process.env.CIVIC_CLIENT_SECRET,
grant_type: "authorization_code",
}),
});
const { access_token } = await tokenResponse.json();
const identityResponse = await fetch(
"https://api.civic.com/identity/attestation",
{ headers: { Authorization: `Bearer ${access_token}` } }
);
const attestation = await identityResponse.json();
const submitMsg = {
submit_kyc: {
user: userAddress,
level: attestation.level,
expiry: attestation.expiry,
country: attestation.country_code,
signature: attestation.signature_hex,
},
};
const tx = await signer.execute(
userAddress,
COMPLIANCE_CONTRACT,
submitMsg,
"auto"
);
return tx.transactionHash;
}
5.2 Polygon ID / zkKYC 集成
Polygon ID 提供基于零知识证明的身份验证方案,用户可以在不泄露原始数据的情况下证明身份。
工作原理
1. 用户生成身份证明(Identity Wallet)
2. Issuer 签发可验证凭证(VC)
3. 用户生成零知识证明
4. 验证者验证 ZK 证明(链上或链下)
5. 验证通过后标记合规状态
集成代码
// polygon-id-integration.ts
import { CredentialWallet, IdentityWallet } from "@0xpolygonid/js-sdk";
import { CircuitId } from "@0xpolygonid/js-sdk";
export class PolygonIDProvider {
private issuerDid: string;
private rpcUrl: string;
constructor(issuerDid: string, rpcUrl: string) {
this.issuerDid = issuerDid;
this.rpcUrl = rpcUrl;
}
async issueKYCClaim(
userDid: string,
level: number,
country: string | null
): Promise<{ credential: any; hash: string }> {
const credential = {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential", "KYCStatus"],
issuer: this.issuerDid,
credentialSubject: {
id: userDid,
kycLevel: level,
country: country || "unknown",
timestamp: Date.now(),
},
};
const hash = await this.computeCredentialHash(credential);
return { credential, hash };
}
async verifyZKProof(
proof: any,
requiredLevel: number
): Promise<boolean> {
const verificationResult = await fetch(
`${this.rpcUrl}/polygon-id/verify`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
proof,
required_level: requiredLevel,
circuit_id: CircuitId.AtomicQuerySigV2,
}),
}
);
const { verified } = await verificationResult.json();
return verified;
}
async generateProof(
credential: any,
userPrivateKey: string
): Promise<any> {
const proof = {
credential,
circuitId: "credentialAtomicQuerySigV2",
provingParams: {
query: {
schema: "kycLevel",
operator: ">=",
value: 1,
},
},
};
return proof;
}
private async computeCredentialHash(
credential: any
): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(credential));
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
}
链上验证流程图
用户(MSG 钱包) Polygon ID Issuer 合规合约
│ │ │
│ 1. 请求签发凭证 │ │
├──────────────────────────────►│ │
│ │ 2. 验证身份 │
│ │ 签发 VC │
│ 3. 获得 VC │ │
│◄──────────────────────────────┤ │
│ │ │
│ 4. 生成 ZK 证明 │ │
│ (证明 KYC 等级 ≥ 1) │ │
│ │ │
│ 5. 提交 ZK 证明 │ │
│──────────────────────────────────────────────────────────►│
│ │ │
│ │ 6. 链上验证 ZK 证明 │
│ │ 标记为已验证 │
│ │ │
│ 7. KYC 完成 │ │
│◄───────────────────────────────────────────────────────────│
5.3 Worldcoin 集成
Worldcoin 提供基于虹膜扫描的身份验证,特别适合反 Sybil 攻击场景。
// worldcoin-integration.ts
export class WorldcoinProvider {
private appId: string;
private actionName: string;
constructor(appId: string, actionName: string = "kyc-verification") {
this.appId = appId;
this.actionName = actionName;
}
async verifyWithWorldcoin(
userAddress: string
): Promise<WorldcoinResult> {
const verification = await this.requestVerification();
if (!verification.success) {
return { success: false, error: "Worldcoin verification failed" };
}
const isValid = await this.verifyProof(verification.proof);
if (!isValid) {
return { success: false, error: "Invalid proof" };
}
return {
success: true,
level: 2,
merkleRoot: verification.merkleRoot,
nullifierHash: verification.nullifierHash,
attestation: verification.proof,
};
}
private async requestVerification(): Promise<any> {
return {
success: true,
proof: "0x...",
merkleRoot: "0x...",
nullifierHash: "0x...",
};
}
private async verifyProof(proof: string): Promise<boolean> {
const response = await fetch(
`https://developer.worldcoin.org/api/v1/verify/${this.appId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
proof,
action: this.actionName,
}),
}
);
const result = await response.json();
return result.success;
}
}
interface WorldcoinResult {
success: boolean;
error?: string;
level?: number;
merkleRoot?: string;
nullifierHash?: string;
attestation?: string;
}
5.4 自定义 Provider 实现
当第三方 Provider 不满足需求时,可以构建自己的 KYC/AML 服务。
后端 API 设计
// custom-provider/server.ts
import express from "express";
import cors from "cors";
import { ethers } from "ethers";
import { ComplianceClient } from "./compliance-client";
const app = express();
app.use(cors());
app.use(express.json());
const complianceClient = new ComplianceClient(
process.env.MSG_PRIVATE_KEY!,
process.env.COMPLIANCE_CONTRACT!
);
app.post("/api/kyc/start", async (req, res) => {
const { userAddress, level } = req.body;
if (!userAddress || !level) {
return res.status(400).json({ error: "Missing required fields" });
}
const session = await createKYCSession(userAddress, level);
res.json({
sessionId: session.id,
uploadUrl: `/api/kyc/upload/${session.id}`,
expiresIn: 3600,
});
});
app.post("/api/kyc/upload/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const files = req.files;
if (!files) {
return res.status(400).json({ error: "No files uploaded" });
}
const extractedData = await processIdentityDocuments(files);
await updateKYCSession(sessionId, { documentsProcessed: true, extractedData });
res.json({ status: "processing", estimatedTime: 120 });
});
app.post("/api/kyc/verify/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const { approved, reason } = req.body;
const session = await getKYCSession(sessionId);
if (!session) return res.status(404).json({ error: "Session not found" });
if (approved) {
const amlResult = await performAMLCheck(session.extractedData);
if (amlResult.flagged) {
return res.status(403).json({
error: "AML check failed",
details: amlResult.reasons,
});
}
const attestation = await complianceClient.submitKYC(
session.userAddress,
session.level,
session.extractedData.country
);
await cleanupSessionData(sessionId);
return res.json({
status: "verified",
level: session.level,
txHash: attestation.txHash,
});
} else {
await updateKYCSession(sessionId, { status: "rejected", reason });
return res.json({ status: "rejected", reason });
}
});
app.get("/api/kyc/status/:userAddress", async (req, res) => {
const { userAddress } = req.params;
const status = await complianceClient.getStatus(userAddress);
res.json({
address: userAddress,
level: status.level,
expiry: status.expiry,
country: status.country,
paused: status.paused,
});
});
app.post("/api/aml/check", async (req, res) => {
const { name, dateOfBirth, country, address } = req.body;
const result = await performAMLCheck({
fullName: name,
dob: dateOfBirth,
nationality: country,
residentialAddress: address,
});
res.json({
flagged: result.flagged,
riskLevel: result.riskLevel,
reasons: result.reasons,
checkId: result.checkId,
timestamp: result.timestamp,
});
});
class ComplianceClient {
private signer: any;
private contract: any;
constructor(privateKey: string, contractAddress: string) {
const provider = new ethers.JsonRpcProvider("https://rpc.msgchain.org");
this.signer = new ethers.Wallet(privateKey, provider);
}
async submitKYC(
userAddress: string,
level: number,
country: string | null
): Promise<{ txHash: string }> {
const msg = {
submit_kyc: {
user: userAddress,
level,
expiry: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
country: country || null,
signature: "0x",
},
};
return { txHash: "0x..." };
}
async getStatus(userAddress: string): Promise<any> {
return { level: 2, expiry: 0, country: "CN", paused: false };
}
async revokeKYC(userAddress: string): Promise<void> {}
}
async function createKYCSession(userAddress: string, level: number): Promise<{ id: string }> {
return { id: `session_${Date.now()}` };
}
async function getKYCSession(sessionId: string): Promise<any> {
return {
id: sessionId,
userAddress: "msg1...",
level: 2,
documentsProcessed: true,
extractedData: {
fullName: "张三",
dateOfBirth: "1990-01-01",
nationality: "CN",
documentNumber: "110101199001011234",
country: "CN",
},
};
}
async function updateKYCSession(sessionId: string, updates: Record<string, any>): Promise<void> {}
async function processIdentityDocuments(files: any): Promise<Record<string, any>> {
return {
fullName: "张三",
dateOfBirth: "1990-01-01",
nationality: "CN",
documentNumber: "110101199001011234",
};
}
async function performAMLCheck(
identity: Record<string, any>
): Promise<{
flagged: boolean;
riskLevel: "low" | "medium" | "high";
reasons: string[];
checkId: string;
timestamp: string;
}> {
const amlService = new AMLCheckService();
return amlService.screen(identity);
}
async function cleanupSessionData(sessionId: string): Promise<void> {}
app.listen(3000, () => {
console.log("KYC/AML service running on port 3000");
});
AML 筛查服务实现
// custom-provider/aml-service.ts
export class AMLCheckService {
private sanctionsList: SanctionsEntry[];
private pepDatabase: PEPEntry[];
constructor() {
this.sanctionsList = [];
this.pepDatabase = [];
}
async screen(identity: IdentityData): Promise<AMLResult> {
const reasons: string[] = [];
let riskLevel: "low" | "medium" | "high" = "low";
let flagged = false;
const sanctionsHit = await this.checkSanctionsList(identity);
if (sanctionsHit.length > 0) {
flagged = true;
riskLevel = "high";
reasons.push(
...sanctionsHit.map((s) => `Matched sanctions entry: ${s.name} (${s.list})`)
);
}
const pepHit = await this.checkPEPList(identity);
if (pepHit) {
riskLevel = riskLevel === "high" ? "high" : "medium";
reasons.push(`Identified as PEP: ${pepHit.name} (${pepHit.position})`);
}
const countryRisk = this.assessCountryRisk(identity.nationality);
if (countryRisk === "high") {
riskLevel = riskLevel === "high" ? "high" : "medium";
reasons.push(`High-risk country: ${identity.nationality}`);
}
if (!this.isAgeValid(identity.dateOfBirth)) {
flagged = true;
reasons.push("Age verification failed: under minimum age");
}
return {
flagged,
riskLevel,
reasons,
checkId: `aml_${Date.now()}`,
timestamp: new Date().toISOString(),
};
}
private async checkSanctionsList(identity: IdentityData): Promise<SanctionsEntry[]> {
return [];
}
private async checkPEPList(identity: IdentityData): Promise<PEPEntry | null> {
return null;
}
private assessCountryRisk(countryCode: string): "low" | "medium" | "high" {
const highRiskCountries = ["PRK", "IRN", "MMR", "SYR"];
if (highRiskCountries.includes(countryCode)) return "high";
const mediumRiskCountries = ["AFG", "AGO", "AZE", "BHR"];
if (mediumRiskCountries.includes(countryCode)) return "medium";
return "low";
}
private isAgeValid(dateOfBirth: string): boolean {
const dob = new Date(dateOfBirth);
const now = new Date();
const age = now.getFullYear() - dob.getFullYear();
return age >= 18;
}
}
interface IdentityData {
fullName: string;
dateOfBirth: string;
nationality: string;
residentialAddress?: string;
documentNumber?: string;
}
interface SanctionsEntry {
name: string;
list: string;
reason: string;
}
interface PEPEntry {
name: string;
position: string;
country: string;
}
interface AMLResult {
flagged: boolean;
riskLevel: "low" | "medium" | "high";
reasons: string[];
checkId: string;
timestamp: string;
}
5.5 Provider 配置对比
| 特性 | Civic | Polygon ID | Worldcoin | 自定义 |
|---|---|---|---|---|
| 部署难度 | 低(SDK 成熟) | 中 | 低 | 高 |
| 隐私保护 | 低 | 高(ZK) | 中 | 可配置 |
| 反 Sybil | 中 | 低 | 高 | 中 |
| 全球覆盖 | 是 | 是 | 有限 | 自定 |
| 合规证书 | 是 | 是 | 否 | 自建 |
| 成本 | 按次付费 | 免费 | 免费 | 按基础设施 |
| 集成时间 | 1-2 天 | 3-5 天 | 1 天 | 2-4 周 |
| 用户设备要求 | 智能手机 | 无特殊要求 | Orb 设备 | 视方案而定 |
| AML 筛查 | 内置 | 需额外集成 | 不提供 | 自建或第三方 |
6. 前端集成
6.1 KYC 状态组件
// KYCStatusBadge.tsx
import React from "react";
import { useCompliance } from "./useCompliance";
interface KYCStatusBadgeProps {
userAddress: string;
onStartKYC?: () => void;
}
const LEVEL_LABELS: Record<number, string> = {
0: "未验证",
1: "基础认证",
2: "高级认证",
3: "企业认证",
};
const LEVEL_COLORS: Record<number, string> = {
0: "#6b7280",
1: "#3b82f6",
2: "#10b981",
3: "#8b5cf6",
};
export const KYCStatusBadge: React.FC<KYCStatusBadgeProps> = ({
userAddress,
onStartKYC,
}) => {
const { status, loading, error } = useCompliance(userAddress);
if (loading) {
return (
<div className="kyc-badge kyc-loading">
<span className="spinner" />
<span>加载中...</span>
</div>
);
}
if (error) {
return (
<div className="kyc-badge kyc-error">
<span>{error}</span>
</div>
);
}
if (!status || !status.is_valid) {
return (
<div className="kyc-badge kyc-unverified">
<span>未验证</span>
{onStartKYC && (
<button className="kyc-start-btn" onClick={onStartKYC}>
开始验证
</button>
)}
</div>
);
}
return (
<div
className="kyc-badge kyc-verified"
style={{ borderColor: LEVEL_COLORS[status.level] }}
>
<span className="kyc-level" style={{ color: LEVEL_COLORS[status.level] }}>
{LEVEL_LABELS[status.level]}
</span>
<span className="kyc-expiry">
有效期至: {new Date(status.expiry).toLocaleDateString("zh-CN")}
</span>
</div>
);
};
6.2 合规 Hook
// useCompliance.ts
import { useState, useEffect, useCallback } from "react";
import { ComplianceSDK, KYCStatusInfo } from "./compliance-sdk";
interface UseComplianceResult {
status: KYCStatusInfo | null;
loading: boolean;
error: string | null;
refresh: () => Promise<void>;
}
export function useCompliance(
userAddress: string | undefined
): UseComplianceResult {
const [status, setStatus] = useState<KYCStatusInfo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchStatus = useCallback(async () => {
if (!userAddress) {
setStatus(null);
setLoading(false);
return;
}
setLoading(true);
setError(null);
try {
const sdk = await ComplianceSDK.connect();
const result = await sdk.getStatus(userAddress);
setStatus(result);
} catch (err: any) {
setError(err.message || "Failed to fetch KYC status");
} finally {
setLoading(false);
}
}, [userAddress]);
useEffect(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 60_000);
return () => clearInterval(interval);
}, [fetchStatus]);
return { status, loading, error, refresh: fetchStatus };
}
6.3 合规性检查 Hook
// useComplianceCheck.ts
import { useState, useCallback } from "react";
import { ComplianceSDK } from "./compliance-sdk";
interface ComplianceCheckResult {
allowed: boolean;
reason: string | null;
requiredLevel: number;
currentLevel: number;
}
export function useComplianceCheck() {
const [checking, setChecking] = useState(false);
const checkAccess = useCallback(
async (userAddress: string, requiredLevel: number): Promise<ComplianceCheckResult> => {
setChecking(true);
try {
const sdk = await ComplianceSDK.connect();
const result = await sdk.isCompliant(userAddress, requiredLevel);
return {
allowed: result.compliant,
reason: result.reason,
requiredLevel,
currentLevel: result.current_level,
};
} catch (err: any) {
return {
allowed: false,
reason: err.message || "Compliance check failed",
requiredLevel,
currentLevel: 0,
};
} finally {
setChecking(false);
}
},
[]
);
return { checkAccess, checking };
}
6.4 KYC 启动按钮
// KYCStartButton.tsx
import React, { useState } from "react";
import { useWallet } from "./useWallet";
import { CivicProvider } from "./civic-integration";
import { ComplianceSDK } from "./compliance-sdk";
interface KYCStartButtonProps {
targetLevel: 1 | 2 | 3;
provider?: "civic" | "polygon-id" | "worldcoin" | "custom";
onComplete?: (txHash: string) => void;
onError?: (error: string) => void;
}
export const KYCStartButton: React.FC<KYCStartButtonProps> = ({
targetLevel,
provider = "civic",
onComplete,
onError,
}) => {
const { address } = useWallet();
const [loading, setLoading] = useState(false);
const [step, setStep] = useState<"idle" | "redirecting" | "verifying" | "submitting" | "done">("idle");
const handleStartKYC = async () => {
if (!address) {
onError?.("请先连接钱包");
return;
}
setLoading(true);
setStep("redirecting");
try {
let result;
switch (provider) {
case "civic": {
const civic = new CivicProvider(
process.env.NEXT_PUBLIC_CIVIC_CLIENT_ID!,
address
);
result = await civic.startKYC();
break;
}
case "polygon-id": {
const polygonProvider = new PolygonIDProvider(
process.env.NEXT_PUBLIC_POLYGON_ISSUER_DID!,
process.env.NEXT_PUBLIC_RPC_URL!
);
const { credential } = await polygonProvider.issueKYCClaim(
address,
targetLevel,
"CN"
);
const proof = await polygonProvider.generateProof(credential, "");
const verified = await polygonProvider.verifyZKProof(proof, targetLevel);
result = { success: verified, attestation: JSON.stringify(proof) };
break;
}
case "worldcoin": {
const worldcoin = new WorldcoinProvider(
process.env.NEXT_PUBLIC_WORLDCOIN_APP_ID!
);
result = await worldcoin.verifyWithWorldcoin(address);
break;
}
default:
throw new Error("Unsupported provider");
}
if (!result.success) {
throw new Error(result.error || "KYC verification failed");
}
setStep("submitting");
// 提交至链上合规合约
const submitMsg = {
submit_kyc: {
user: address,
level: targetLevel,
expiry: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
country: null,
signature: result.attestation,
},
};
setStep("done");
} catch (err: any) {
setStep("idle");
onError?.(err.message || "KYC process failed");
} finally {
setLoading(false);
}
};
const stepLabels: Record<string, string> = {
idle: `开始 ${targetLevel === 1 ? "基础" : targetLevel === 2 ? "高级" : "企业"}验证`,
redirecting: "正在跳转至验证服务...",
verifying: "身份验证进行中...",
submitting: "正在提交至链上...",
done: "验证完成!",
};
return (
<div className="kyc-start-container">
<button
className={`kyc-start-button ${loading ? "kyc-loading" : ""}`}
onClick={handleStartKYC}
disabled={loading || !address}
>
{loading && <span className="spinner" />}
{stepLabels[step] || stepLabels.idle}
</button>
</div>
);
};
6.5 KYC 页面示例
// KYCPage.tsx
import React, { useState } from "react";
import { useWallet } from "./useWallet";
import { useCompliance } from "./useCompliance";
import { KYCStatusBadge } from "./KYCStatusBadge";
import { KYCStartButton } from "./KYCStartButton";
export const KYCPage: React.FC = () => {
const { address, connect } = useWallet();
const { status, refresh } = useCompliance(address);
const [error, setError] = useState<string | null>(null);
if (!address) {
return (
<div className="kyc-page">
<div className="kyc-connect-prompt">
<h1>KYC 验证</h1>
<p>请先连接您的 MSG 钱包以开始验证流程。</p>
<button className="connect-btn" onClick={connect}>连接钱包</button>
</div>
</div>
);
}
return (
<div className="kyc-page">
<header className="kyc-header">
<h1>KYC 合规验证</h1>
<KYCStatusBadge userAddress={address} />
</header>
<section className="kyc-status-section">
<h2>当前验证信息</h2>
<div className="kyc-info-grid">
<div className="kyc-info-item">
<label>钱包地址</label>
<span className="address">{address}</span>
</div>
<div className="kyc-info-item">
<label>当前等级</label>
<span>{status ? ["未验证", "基础", "高级", "企业"][status.level] : "未验证"}</span>
</div>
<div className="kyc-info-item">
<label>有效期至</label>
<span>{status?.expiry ? new Date(status.expiry).toLocaleDateString("zh-CN") : "—"}</span>
</div>
<div className="kyc-info-item">
<label>国家/地区</label>
<span>{status?.country || "—"}</span>
</div>
</div>
</section>
<section className="kyc-upgrade-section">
<h2>升级验证等级</h2>
<div className="kyc-tier-cards">
{[
{ level: 1, name: "基础认证", limits: "每日 1,000 MSG", features: ["邮箱验证", "手机号验证"] },
{ level: 2, name: "高级认证", limits: "每日 10,000 MSG", features: ["身份证/护照验证", "活体检测", "AML 筛查"] },
{ level: 3, name: "企业认证", limits: "每日 100,000 MSG", features: ["公司文件验证", "法人身份验证", "尽职调查"] },
].map((tier) => (
<div key={tier.level} className="kyc-tier-card">
<h3>{tier.name}</h3>
<ul>{tier.features.map((f, i) => <li key={i}>{f}</li>)}</ul>
<p><strong>交易限额:</strong> {tier.limits}</p>
{(!status || status.level < tier.level) && (
<KYCStartButton
targetLevel={tier.level as 1 | 2 | 3}
onComplete={() => { setError(null); refresh(); }}
onError={setError}
/>
)}
</div>
))}
</div>
</section>
{error && (
<div className="kyc-error-banner">
<span>{error}</span>
<button onClick={() => setError(null)}>✕</button>
</div>
)}
</div>
);
};
6.6 组件目录结构
src/components/compliance/
├── KYCStatusBadge.tsx # KYC 状态徽章
├── KYCStartButton.tsx # 开始 KYC 按钮
├── KYCPage.tsx # KYC 管理页面
├── ComplianceGate.tsx # 合规门控组件
├── TierLimitIndicator.tsx # 限额指示器
├── useCompliance.ts # KYC 状态 Hook
├── useComplianceCheck.ts # 合规检查 Hook
├── compliance-sdk.ts # 链上 SDK
├── civic-integration.ts # Civic 集成
├── polygon-id-integration.ts # Polygon ID 集成
└── worldcoin-integration.ts # Worldcoin 集成
6.7 合规门控组件
// ComplianceGate.tsx
import React from "react";
import { useComplianceCheck } from "./useComplianceCheck";
interface ComplianceGateProps {
userAddress: string;
requiredLevel: number;
fallback?: React.ReactNode;
children: React.ReactNode;
}
export const ComplianceGate: React.FC<ComplianceGateProps> = ({
userAddress,
requiredLevel,
fallback,
children,
}) => {
const { checkAccess, checking } = useComplianceCheck();
const [result, setResult] = React.useState<{
allowed: boolean;
reason: string | null;
} | null>(null);
React.useEffect(() => {
checkAccess(userAddress, requiredLevel).then(setResult);
}, [userAddress, requiredLevel, checkAccess]);
if (checking || !result) {
return <div className="compliance-gate-loading">检查合规状态...</div>;
}
if (!result.allowed) {
if (fallback) return <>{fallback}</>;
return (
<div className="compliance-gate-blocked">
<h3>操作受限</h3>
<p>{result.reason || "请先完成 KYC 验证"}</p>
<p className="gate-level-hint">需要等级: {requiredLevel}</p>
</div>
);
}
return <>{children}</>;
};
7. 隐私考虑
7.1 数据最小化原则
合规架构遵循数据最小化原则,仅收集和存储必要的最小数据量。
收集的数据类型与目的:
┌────────────────────────────────────────────────────────────────┐
│ 身份数据 KYC Provider 持有 目的 │
├────────────────────────────────────────────────────────────────┤
│ 姓名、出生日期 ✓(暂时存储) 身份验证 │
│ 身份证/护照号 ✓(暂时存储) 身份验证 │
│ 地址 ✓(可选) AML 筛查 │
│ 生物特征 ✓(部分 Provider) 活体检测 │
│ 钱包地址 ✓(长期) 链上记录 │
│ KYC 等级 ✓(长期) 合规验证 │
│ 国家代码 ✓(长期) 国别风险评估 │
│ 时间戳 ✓(长期) 审计追踪 │
│ ZK 证明 ✗(链上存储) 隐私保护验证 │
└────────────────────────────────────────────────────────────────┘
7.2 零知识证明方案
使用零知识证明(ZK),用户可以在不泄露原始身份数据的情况下证明:
- 已通过 KYC 验证(是/否)
- KYC 等级 >= 某个值
- 年龄 >= 18 岁
- 不是特定国家的居民
// zk-kyc-verifier.ts
export class ZKKYCVerifier {
async generateLevelProof(
credential: any,
requiredLevel: number,
privateKey: string
): Promise<ZKProof> {
const actualLevel = credential.credentialSubject.kycLevel;
if (actualLevel < requiredLevel) {
throw new Error("Insufficient KYC level");
}
const proof = {
proofType: "level_comparison",
circuitId: "credentialAtomicQuerySigV2",
pubSignals: ["1"],
proof: await this.generateCircuitProof(credential, requiredLevel, privateKey),
};
return proof;
}
async generateAgeProof(
credential: any,
privateKey: string
): Promise<ZKProof> {
const dob = new Date(credential.credentialSubject.dateOfBirth);
const age = this.calculateAge(dob);
if (age < 18) {
throw new Error("Underage");
}
return {
proofType: "age_verification",
circuitId: "ageCredential",
pubSignals: ["1"],
proof: await this.generateCircuitProof(credential, 18, privateKey),
};
}
async generateCountryProof(
credential: any,
restrictedCountries: string[],
privateKey: string
): Promise<ZKProof> {
const userCountry = credential.credentialSubject.country;
if (restrictedCountries.includes(userCountry)) {
throw new Error("Country is restricted");
}
return {
proofType: "country_exclusion",
circuitId: "countryExclusion",
pubSignals: ["1"],
proof: await this.generateCircuitProof(credential, restrictedCountries, privateKey),
};
}
private async generateCircuitProof(
credential: any,
privateInput: any,
privateKey: string
): Promise<string> {
return "0x...";
}
private calculateAge(dob: Date): number {
const now = new Date();
let age = now.getFullYear() - dob.getFullYear();
const monthDiff = now.getMonth() - dob.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < dob.getDate())) {
age--;
}
return age;
}
}
interface ZKProof {
proofType: string;
circuitId: string;
pubSignals: string[];
proof: string;
}
ZK 验证合约示例
// zk-verifier-contract.rs
use cosmwasm_std::{
entry_point, Binary, Deps, DepsMut, Env, MessageInfo,
Response, StdResult,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VerifyZKMsg {
pub proof: Binary,
pub pub_signals: Vec<String>,
pub required_level: u8,
}
#[entry_point]
pub fn verify_kyc_proof(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: VerifyZKMsg,
) -> StdResult<Response> {
let verification_key = load_verification_key(deps.storage)?;
let valid = verify_groth16_proof(
&verification_key,
&msg.proof,
&msg.pub_signals,
);
if !valid {
return Err(cosmwasm_std::StdError::generic_err("ZK proof invalid"));
}
let is_verified = msg.pub_signals
.get(0)
.map(|s| s == "1")
.unwrap_or(false);
if !is_verified {
return Err(cosmwasm_std::StdError::generic_err("Condition not met"));
}
mark_user_verified(deps.storage, &_info.sender, msg.required_level)?;
Ok(Response::new()
.add_attribute("action", "verify_zk_kyc")
.add_attribute("user", _info.sender.to_string())
.add_attribute("level", msg.required_level.to_string()))
}
7.3 数据加密方案
对于存储在 Provider 侧的用户数据,使用以下加密策略:
// data-encryption.ts
import crypto from "crypto";
export class KYCDataEncryption {
private algorithm = "aes-256-gcm";
private key: Buffer;
constructor(encryptionKey: string) {
this.key = crypto.scryptSync(encryptionKey, "kyc-salt", 32);
}
encrypt(data: string): EncryptedData {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(data, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
return {
iv: iv.toString("hex"),
encryptedData: encrypted,
authTag: authTag.toString("hex"),
};
}
decrypt(encrypted: EncryptedData): string {
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(encrypted.iv, "hex")
);
decipher.setAuthTag(Buffer.from(encrypted.authTag, "hex"));
let decrypted = decipher.update(encrypted.encryptedData, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
hash(data: string): string {
return crypto.createHash("sha256").update(data).digest("hex");
}
}
interface EncryptedData {
iv: string;
encryptedData: string;
authTag: string;
}
7.4 GDPR 合规
用户权利支持
// gdpr-compliance.ts
export class GDPRCompliance {
async exportUserData(userAddress: string): Promise<UserDataPackage> {
const identityData = await this.getStoredIdentityData(userAddress);
const kycStatus = await this.getKYCStatus(userAddress);
const auditLog = await this.getAuditLog(userAddress);
return {
exportedAt: new Date().toISOString(),
userAddress,
identityData,
kycStatus,
auditLog,
};
}
async deleteUserData(userAddress: string): Promise<void> {
await this.deleteIdentityData(userAddress);
await this.revokeOnChainKYC(userAddress);
await this.anonymizeAuditLog(userAddress);
await this.recordDeletionRequest(userAddress);
}
async withdrawConsent(userAddress: string): Promise<void> {
await this.updateConsentRecord(userAddress, {
kycConsent: false,
dataProcessingConsent: false,
withdrawnAt: new Date().toISOString(),
});
await this.pauseDataProcessing(userAddress);
}
}
7.5 数据保留政策
数据保留时间表:
┌─────────────────────────────┬──────────────────┬──────────────────────┐
│ 数据类型 │ 保留期限 │ 到期处理方式 │
├─────────────────────────────┼──────────────────┼──────────────────────┤
│ KYC 验证记录 │ 验证期 + 5 年 │ 匿名化 │
│ 身份文件(扫描件) │ 验证完成后 90 天 │ 安全删除(覆写 3 次)│
│ AML 筛查结果 │ 筛查日 + 5 年 │ 匿名化 │
│ 链上合规状态 │ 永久 │ 不可变(仅可撤销) │
│ 审计日志 │ 生成日 + 7 年 │ 归档 │
│ 同意记录 │ 永久 │ 永久保留(合规要求) │
│ Session 数据 │ 24 小时 │ 自动删除 │
│ 通信记录 │ 90 天 │ 自动删除 │
└─────────────────────────────┴──────────────────┴──────────────────────┘
自动删除实现
// data-retention-cleanup.ts
import cron from "node-cron";
export class DataRetentionCleanup {
startScheduledCleanup(): void {
cron.schedule("0 3 * * *", async () => {
console.log("Starting data retention cleanup...");
await Promise.all([
this.cleanupExpiredSessions(),
this.cleanupOldIdentityDocuments(),
this.anonymizeExpiredRecords(),
this.archiveAuditLogs(),
]);
console.log("Cleanup completed");
});
}
private async cleanupExpiredSessions(): Promise<void> {
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
// await deleteSessionsOlderThan(cutoff);
}
private async cleanupOldIdentityDocuments(): Promise<void> {
const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
// await deleteDocumentsOlderThan(cutoff);
}
private async anonymizeExpiredRecords(): Promise<void> {
// await anonymizeExpiredKYCRecords();
}
private async archiveAuditLogs(): Promise<void> {
const cutoff = new Date(Date.now() - 7 * 365 * 24 * 60 * 60 * 1000);
// await archiveLogsOlderThan(cutoff);
}
}
8. 法律与合规
8.1 司法管辖区特定要求
司法管辖区 KYC/AML 要求概览:
┌─────────────────────┬──────────┬──────────┬──────────┬───────────┐
│ 要求 │ 美国 │ 欧盟 │ 新加坡 │ 香港 │
├─────────────────────┼──────────┼──────────┼──────────┼───────────┤
│ KYC 必须 │ ✓ │ ✓ │ ✓ │ ✓ │
│ AML 程序 │ ✓ │ ✓ │ ✓ │ ✓ │
│ 制裁筛查 │ ✓ (OFAC) │ ✓ (EU) │ ✓ (UN) │ ✓ (UN) │
│ PEP 检查 │ ✓ │ ✓ │ ✓ │ ✓ │
│ 交易监控 │ ✓ │ ✓ │ ✓ │ ✓ │
│ 可疑交易报告 │ ✓ │ ✓ │ ✓ │ ✓ │
│ 记录保存(年) │ 5 │ 5 │ 5 │ 7 │
│ 旅行规则 │ ✓ │ ✓ │ ✓ │ ✓ │
│ 数据本地化 │ ✗ │ ✓ (GDPR) │ ✗ │ ✗ │
│ 加密货币特定许可 │ 各州不同 │ MiCA │ PSA │ SFO │
└─────────────────────┴──────────┴──────────┴──────────┴───────────┘
8.2 制裁筛查实现
// sanctions-screening.ts
export class SanctionsScreener {
private ofacList: SanctionsEntry[];
private euList: SanctionsEntry[];
private unList: SanctionsEntry[];
constructor() {
this.ofacList = [];
this.euList = [];
this.unList = [];
}
async updateSanctionsLists(): Promise<void> {
const [ofac, eu, un] = await Promise.all([
this.fetchOFACList(),
this.fetchEUSanctionsList(),
this.fetchUNSanctionsList(),
]);
this.ofacList = ofac;
this.euList = eu;
this.unList = un;
}
async screen(name: string, dateOfBirth: string, country: string): Promise<SanctionsResult> {
const hits: SanctionHit[] = [];
const normalizedName = this.normalizeName(name);
for (const entry of this.ofacList) {
if (this.isMatch(normalizedName, entry.name)) {
hits.push({ list: "OFAC", entry: entry.name, matchType: this.getMatchType(normalizedName, entry.name) });
}
}
for (const entry of this.euList) {
if (this.isMatch(normalizedName, entry.name)) {
hits.push({ list: "EU", entry: entry.name, matchType: this.getMatchType(normalizedName, entry.name) });
}
}
for (const entry of this.unList) {
if (this.isMatch(normalizedName, entry.name)) {
hits.push({ list: "UN", entry: entry.name, matchType: this.getMatchType(normalizedName, entry.name) });
}
}
await this.recordScreeningResult({
screenedAt: new Date().toISOString(),
name, dateOfBirth, country, hits, passed: hits.length === 0,
});
return { passed: hits.length === 0, hits, riskLevel: hits.length > 0 ? "high" : "low" };
}
private normalizeName(name: string): string {
return name.toLowerCase().replace(/[^a-z\s]/g, "").replace(/\s+/g, " ").trim();
}
private isMatch(normalizedInput: string, entryName: string): boolean {
const normalizedEntry = this.normalizeName(entryName);
if (normalizedInput === normalizedEntry) return true;
const inputParts = normalizedInput.split(" ");
const entryParts = normalizedEntry.split(" ");
const common = inputParts.filter((p) => entryParts.includes(p));
return common.length >= Math.min(inputParts.length, entryParts.length) * 0.7;
}
private getMatchType(normalizedInput: string, entryName: string): "exact" | "partial" | "fuzzy" {
if (this.normalizeName(entryName) === normalizedInput) return "exact";
if (this.isMatch(normalizedInput, entryName)) return "partial";
return "fuzzy";
}
private async fetchOFACList(): Promise<SanctionsEntry[]> {
const response = await fetch("https://www.treasury.gov/ofac/downloads/sdn.xml");
const xml = await response.text();
return this.parseOFACXML(xml);
}
private async fetchEUSanctionsList(): Promise<SanctionsEntry[]> { return []; }
private async fetchUNSanctionsList(): Promise<SanctionsEntry[]> { return []; }
private parseOFACXML(xml: string): SanctionsEntry[] { return []; }
private async recordScreeningResult(result: ScreeningRecord): Promise<void> {}
}
interface SanctionsEntry { name: string; aliases: string[]; list: string; }
interface SanctionHit { list: string; entry: string; matchType: "exact" | "partial" | "fuzzy"; }
interface SanctionsResult { passed: boolean; hits: SanctionHit[]; riskLevel: "low" | "medium" | "high"; }
interface ScreeningRecord { screenedAt: string; name: string; dateOfBirth: string; country: string; hits: SanctionHit[]; passed: boolean; }
8.3 旅行规则合规
旅行规则(Travel Rule)要求虚拟资产服务提供商(VASP)在传输交易时传递发送方和接收方的身份信息。
// travel-rule-compliance.ts
export class TravelRuleCompliance {
private threshold = 1000;
checkTravelRuleRequirement(amount: Uint128, tokenPrice: number): boolean {
const usdValue = Number(amount) * tokenPrice;
return usdValue >= this.threshold;
}
buildTravelRuleData(
sender: TravelRuleParty,
receiver: TravelRuleParty,
amount: string,
asset: string
): TravelRulePayload {
return {
originator: {
name: sender.name,
address: sender.address,
dateOfBirth: sender.dateOfBirth,
country: sender.country,
walletAddress: sender.walletAddress,
},
beneficiary: {
name: receiver.name,
address: receiver.address,
walletAddress: receiver.walletAddress,
},
transaction: { amount, asset, timestamp: new Date().toISOString() },
};
}
async transmitTravelRuleData(targetVASP: string, payload: TravelRulePayload): Promise<void> {
const encrypted = await this.encryptPayload(payload);
const signature = await this.signPayload(encrypted);
await fetch(`https://${targetVASP}/travel-rule/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload: encrypted, signature, senderVASP: "msgchain-kyc.msg.com" }),
});
}
private async encryptPayload(payload: TravelRulePayload): Promise<string> {
return "encrypted_payload";
}
private async signPayload(payload: string): Promise<string> {
return "signature";
}
}
interface TravelRuleParty {
name: string;
address: string;
dateOfBirth?: string;
country?: string;
walletAddress: string;
}
interface TravelRulePayload {
originator: { name: string; address: string; dateOfBirth?: string; country?: string; walletAddress: string; };
beneficiary: { name: string; address: string; walletAddress: string; };
transaction: { amount: string; asset: string; timestamp: string; };
}
8.4 记录保存
// record-keeping.ts
export class AuditRecordKeeper {
async recordKYCEvent(event: KYCEvent): Promise<void> {
const record = {
id: generateUUID(),
type: "kyc_event",
timestamp: new Date().toISOString(),
data: event,
chain: "msg-chain-1",
hash: await this.computeHash(event),
};
await this.saveAuditRecord(record);
}
async recordAMLEvent(event: AMLEvent): Promise<void> {
const record = {
id: generateUUID(),
type: "aml_event",
timestamp: new Date().toISOString(),
data: event,
chain: "msg-chain-1",
hash: await this.computeHash(event),
};
await this.saveAuditRecord(record);
}
async recordTransactionEvent(event: TransactionEvent): Promise<void> {
const record = {
id: generateUUID(),
type: "transaction_event",
timestamp: new Date().toISOString(),
data: event,
chain: "msg-chain-1",
hash: await this.computeHash(event),
};
await this.saveAuditRecord(record);
}
async exportRecords(startDate: string, endDate: string, type?: string): Promise<AuditRecord[]> {
let query: any = { timestamp: { $gte: startDate, $lte: endDate } };
if (type) query.type = type;
return this.queryAuditRecords(query);
}
async verifyRecordIntegrity(recordId: string): Promise<boolean> {
const record = await this.getAuditRecord(recordId);
if (!record) return false;
const computedHash = await this.computeHash(record.data);
return computedHash === record.hash;
}
private async computeHash(data: any): Promise<string> {
const encoder = new TextEncoder();
const buffer = encoder.encode(JSON.stringify(data));
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
private async saveAuditRecord(record: AuditRecord): Promise<void> {}
private async queryAuditRecords(query: any): Promise<AuditRecord[]> { return []; }
private async getAuditRecord(id: string): Promise<AuditRecord | null> { return null; }
}
interface KYCEvent {
userAddress: string;
action: "submitted" | "approved" | "rejected" | "revoked" | "expired";
level: number;
provider: string;
reviewer?: string;
reason?: string;
txHash?: string;
}
interface AMLEvent {
userAddress: string;
checkType: "sanctions" | "pep" | "adverse_media";
result: "pass" | "fail" | "review";
riskLevel: "low" | "medium" | "high";
matchedList?: string;
reviewer?: string;
}
interface TransactionEvent {
txHash: string;
sender: string;
receiver: string;
amount: string;
asset: string;
travelRuleApplied: boolean;
travelRulePayload?: any;
}
interface AuditRecord {
id: string;
type: string;
timestamp: string;
data: any;
chain: string;
hash: string;
}
function generateUUID(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
8.5 合规声明
本指南中描述的 KYC/AML 合规框架设计用于满足以下监管标准:
1. FATF 建议(特别是建议 15、16、24、25)
2. EU 第 5 号/第 6 号反洗钱指令(5AMLD/6AMLD)
3. 美国银行保密法(BSA)/ 爱国者法案
4. 新加坡支付服务法(PSA)
5. 香港反洗钱及反恐融资条例(AMLO)
6. 中国网络安全法 / 个人数据保护法(跨境场景)
免责声明:本指南仅供参考,不构成法律建议。部署合规系统前,应咨询有经验的区块链和金融监管律师。不同司法管辖区的法律要求可能随时间变化,开发者应持续关注监管动态。
8.6 紧急合规流程
// emergency-compliance.ts
export class EmergencyComplianceActions {
async freezeAddress(address: string, reason: string, authority: string): Promise<void> {
await this.pauseUserOnChain(address);
await this.notifyDApps(address, "frozen");
await this.recordLegalRequest({
type: "freeze",
address,
reason,
authority,
timestamp: new Date().toISOString(),
});
}
async unfreezeAddress(address: string, authority: string): Promise<void> {
await this.unpauseUserOnChain(address);
await this.notifyDApps(address, "unfrozen");
}
async submitSuspiciousActivityReport(transactions: string[], reason: string): Promise<void> {
const report = {
id: generateUUID(),
type: "SAR",
transactions,
reason,
submitter: "msgchain-kyc",
timestamp: new Date().toISOString(),
};
await this.sendToRegulator(report);
await this.retainReport(report);
}
private async pauseUserOnChain(address: string): Promise<void> {}
private async unpauseUserOnChain(address: string): Promise<void> {}
private async notifyDApps(address: string, event: string): Promise<void> {}
private async recordLegalRequest(request: any): Promise<void> {}
private async sendToRegulator(report: any): Promise<void> {}
private async retainReport(report: any): Promise<void> {}
}
附录
A. 常用工具与库
| 工具/库 | 用途 | 链接 |
|---|---|---|
| CosmJS | Cosmos SDK TypeScript 客户端 | https://github.com/cosmos/cosmjs |
| CosmWasm | Rust 智能合约框架 | https://github.com/CosmWasm/cosmwasm |
| Civic SDK | Civic 身份验证 SDK | https://github.com/civicteam/civic-auth |
| Polygon ID SDK | 零知识身份 SDK | https://github.com/0xPolygonID/js-sdk |
| Worldcoin SDK | World ID 验证 SDK | https://github.com/worldcoin/idkit-js |
| ComplyAdvantage | AML 筛查 API | https://complyadvantage.com |
B. 关键术语表
| 术语 | 英文 | 说明 |
|---|---|---|
| 了解你的客户 | KYC | 验证客户身份的过程 |
| 反洗钱 | AML | 防止洗钱活动的法规和措施 |
| 政治公众人物 | PEP | 担任重要公职的人员 |
| 外国资产控制办公室 | OFAC | 美国财政部下属制裁执行机构 |
| 旅行规则 | Travel Rule | FATF 建议的资金转移信息传递规则 |
| 可疑活动报告 | SAR | 向监管机构提交的可疑交易报告 |
| 可验证凭证 | VC | 数字签名的身份声明 |
| 零知识证明 | ZK Proof | 不泄露信息本身而证明信息真实性的密码学方法 |
| 金融行动特别工作组 | FATF | 制定反洗钱国际标准的政府间组织 |
C. 参考资源
- MSG Chain 白皮书系统: https://msgchain.org/whitepaper/
- FATF 虚拟资产指南: https://www.fatf-gafi.org/publications/fatfrecommendations/documents/guidance-rba-virtual-assets.html
- CosmWasm 文档: https://docs.cosmwasm.com
- MSG Chain 开发者门户: https://docs.msgchain.org
- OFAC 制裁名单: https://www.treasury.gov/resource-center/sanctions/SDN-List
- EU 制裁地图: https://www.sanctionsmap.eu
- ISO 3166-1 国家代码: https://www.iso.org/iso-3166-country-codes.html
