A blockchain micro-lottery, dismantled. 一份链上夺宝平台,拆开来看
Duobao is a Telegram-native ticket draw where users buy 1 USDT seats and the winner takes ~85% of the pool. The interesting part is not the game — it's the nine-service split, the pluggable draw engine, and the discipline around custody. This page walks the system end-to-end. 夺宝是一个原生于 Telegram 的购票开奖平台,用户花 1 USDT 买一个号, 中奖者拿走奖池约 85%。有意思的不是玩法本身——是九服务拆分、可插拔的开奖引擎、 以及围绕私钥托管的纪律。本页从头到尾走一遍这套系统。
docs/PRD-V1.0.md ·
docs/3-arch/{overview,backend,on-chain}.md ·
Swagger/SpringDoc API artifact ·
docs/8-tech-debt.mdCode:代码:
duobao-backend/ (Spring Boot · Java 21 · Gradle multi-module)(Spring Boot · Java 21 · Gradle 多模块)
·
Contracts:合约: duobao-contracts/ (Foundry · Solidity 0.8.26)
What it is是什么
the elevator pitch电梯演讲A user opens the Telegram mini-app, deposits USDT on BSC (or BTC on Bitcoin), buys one or more 1-USDT seats in a round, and waits for the round to sell out. When it does, the platform draws a winner via a commit-reveal seed that anyone can verify locally — no gas, no Chainlink, no on-chain selection. Winnings settle to the platform balance ledger immediately; withdrawals are a separate, audited path with multi-sig custody on the cold-tier. 用户打开 Telegram 小程序,把 BSC 上的 USDT (或 Bitcoin 上的 BTC)充值进来,在某一轮里买一个或多个 1 USDT 号码, 等本轮售罄。售罄之后,平台用 commit-reveal 种子开奖,任何人都可以本地验证 —— 不消耗 gas、不依赖 Chainlink、不上链选号。奖金即时落到平台余额账本; 提现走独立、可审计的路径,冷钱包多签托管。
The product surface is small. The plumbing is the point: nine independent Spring Boot services, each with its own MySQL schema, talk over Feign (synchronous) and Kafka (asynchronous, at-least-once with consumer-side idempotency). Sign-service holds the private keys and signs blindly. Wallet-service is the only source of truth for balances. Chain-service is the only thing that talks to a node. Everyone else is forbidden from doing those things — by code, by review, and by boot validators that refuse to start a prod profile when a stub is still wired in.
501.
Ethereum mainnet is config-ready but not a v1.0 launch chain. VRF and full-chain
draw modes are coded as interfaces and stubbed bodies; v1.0 ships the
BACKEND commit-reveal mode only.
Four locked decisions四个锁定的决策
pre-dating the doc pipeline早于文档流水线These were settled before the architecture docs ran. Every later document honors them; reversing one is a re-architecture, not a refactor. 这四条早在架构文档启动之前就定下了。后续每一份文档都遵循它们; 反转任何一条都属于重新设计架构,不是重构。
Nine-service split, financial isolation at the schema九服务拆分,在 schema 层做资金隔离
gateway · user · game · wallet ·
chain · sign · agent ·
admin — plus auxiliary risk & message services. Per-service
MySQL schema, no cross-service joins, no shared transactions. Wallet-service
owns the balance ledger as the single source of truth; sign-service owns the
private keys with no access to business state. Cross-service consistency rides
on Kafka + idempotency keys, not 2PC.
gateway · user · game · wallet ·
chain · sign · agent ·
admin,加上辅助的 risk 和 message。每个服务一个独立 MySQL schema,
不允许跨服务 join,不共享事务。wallet-service 是余额账本的唯一权威来源;sign-service 持有私钥,
但完全不接触业务状态。跨服务一致性靠 Kafka + 幂等键保证,不用 2PC。
Why → blast radius containment. A bug in agent-service can never corrupt a balance. 原因 → 爆炸半径收敛。agent-service 里的 bug 永远污染不了余额。
Pluggable draw engine via a single interface通过单一接口实现可插拔的开奖引擎
One DrawEngine contract in duobao-api, three implementations
(BACKEND, VRF_ONLY, FULL_CHAIN) wired by
@ConditionalOnProperty against one.draw.mode sourced from
Nacos. v1.0 ships BACKEND; the other two are real classes with stub
bodies that throw FEATURE_NOT_IMPLEMENTED so swapping modes never requires
touching game-service.
duobao-api 里一个 DrawEngine 接口,三种实现
(BACKEND / VRF_ONLY / FULL_CHAIN),
通过 @ConditionalOnProperty 读取 Nacos 里的 one.draw.mode 切换。
v1.0 上线 BACKEND;另外两个是真实类但桩体抛 FEATURE_NOT_IMPLEMENTED,
这样切换模式永远不需要改 game-service。
Why → defer the on-chain VRF risk without writing a future migration. 原因 → 把链上 VRF 风险推迟,同时不需要写未来的迁移代码。
Multi-chain via ChainAdapter, one adapter per dialect多链通过 ChainAdapter 解耦,每条方言一个适配器
A single interface (isAddressValid, getBalance,
scanBlock, broadcastSignedTx, getReceipt,
requiredConfirmations) plus per-chain dialect classes. Adding a chain
means writing one adapter and one Nacos config block — business code is untouched.
v1.0 ships EvmChainAdapter (BSC, ETH, Anvil) and a parallel
BtcChainAdapter; TRON is a StubChainAdapter returning
501.
单一接口(isAddressValid / getBalance /
scanBlock / broadcastSignedTx / getReceipt /
requiredConfirmations)加每条链方言类。新增一条链 = 写一个 adapter + 一段 Nacos 配置,
业务代码不动。v1.0 上线 EvmChainAdapter(BSC / ETH / Anvil)和并行的
BtcChainAdapter;TRON 是 StubChainAdapter,返回 501。
Why → chains are a moving target. The interface won't move. 原因 → 链一直在变,接口不变。
Environment and chain are orthogonal — never collapsed环境与链是正交的——永远不合并成一个字段
env ∈ {LOCAL, TESTNET, STAGING,
PRODUCTION} and executionChain ∈ {ETH_LOCAL,
ETH_SEPOLIA, ETH_MAINNET, BSC_LOCAL,
BSC_TESTNET, BSC_MAINNET, TRON_LOCAL,
TRON_TESTNET, TRON_MAINNET, BTC_LOCAL,
BTC_TESTNET, BTC_MAINNET} are independent enums. Any
environment can target any chain; Nacos namespace isolates per-env config, and a
chain_config.${executionChain} block holds per-chain knobs. Common mistakes —
"testnet means Sepolia," "prod means mainnet" — are designed out.
env ∈ {LOCAL, TESTNET, STAGING,
PRODUCTION} 和 executionChain ∈ {ETH_LOCAL,
ETH_SEPOLIA, ETH_MAINNET, BSC_LOCAL,
BSC_TESTNET, BSC_MAINNET, TRON_LOCAL,
TRON_TESTNET, TRON_MAINNET, BTC_LOCAL,
BTC_TESTNET, BTC_MAINNET} 是互相独立的枚举。任何环境都可以指向任何链;
Nacos namespace 按环境隔离配置,chain_config.${executionChain} 段保存每条链的旋钮。
常见错误——"testnet 就是 Sepolia"、"prod 就是 mainnet"——在设计阶段就排除了。
Why → STAGING on mainnet for pre-launch capital tests is a real workflow. 原因 → STAGING 跑在主网上做预发资金测试是真实的工作流。
Service map服务地图
who owns what谁负责什么
A dozen modules live under duobao-backend/. Most are production-ready,
a couple are partial (clearly demarcated), a couple are stub-only.
The map below groups by tier — edge, money, chain, auxiliary.
duobao-backend/ 下大约一打模块。大多数已生产就绪,
少数处于部分实现(明确标注),个别只有桩。下面的地图按层分组——边缘、资金、链路、辅助。
initData validation, injects X-User-Id downstream.keyId, payload) — never business params. Every call audited.Three draw modes, one shipped三种开奖模式,只上线一种
pluggable randomness可插拔的随机性The product spec asks for three trust models, ranked by how much of the draw lives on-chain. v1.0 ships only the leftmost; the other two are present as interface implementations with stub bodies so a future flip is a config change, not a rewrite. 产品规格要求三种信任模型,按"开奖逻辑落在链上的比例"排序。 v1.0 只上线最左边那个;另外两个以接口 + 桩体的方式存在, 未来切换是改一行配置,不是重写代码。
| BACKEND | VRF_ONLY | FULL_CHAIN | |
|---|---|---|---|
| Trust model | Platform commit-reveal off-chain. EIP-191 signed commit published before the round; seed revealed post-draw. Anyone verifies SHA-256 locally. |
Chainlink VRFv2.5 randomness; winner selected off-chain in wallet-service from the verified random word. | On-chain selection & payout. DrawCoordinator contract executes; USDT transfers from DuobaoVault directly to winner. |
| Gas budget | 0 — no on-chain ops in the draw path | requestRandom ≈ 150 k · fulfillRandomWords ≈ 200 k |
VRF + selection + payout ≈ 800 k |
| Latency | < 1 s (DB write + Kafka emit) | 1–3 min (Chainlink callback) + selection window | 1–3 min VRF + block confirms |
| Failure mode | Operator never reveals → audit + admin manual override. Reveal leaked early → seed rotation + round void. | LINK subscription depleted → fall back to BACKEND. 30-min timeout → DRAW_TIMEOUT state. |
Same as VRF + payout reentrancy (mitigated: nonReentrant + SafeERC20). |
| v1.0 status | shipped | stub (TD-59) | stub (TD-59) |
// duobao-api · the single contract every mode honors public interface DrawEngine { DrawResult draw(DrawContext ctx); // initiate DrawMode supportedMode(); default boolean supportsAsyncCallback() { return false; } } // game-service · wiring @Bean @ConditionalOnProperty(name = "one.draw.mode", havingValue = "BACKEND") DrawEngine backendEngine(...) { return new BackendCommitRevealEngine(...); }
one.draw.mode when VRF lands.
Chain layer链路层
one interface, many dialects一个接口,多种方言
Every chain interaction goes through a single ChainAdapter interface.
A new chain is one adapter + one Nacos config block. Business code doesn't know
if it's talking to BSC or Bitcoin.
所有链上交互走单一 ChainAdapter 接口。
新增一条链 = 一个 adapter + 一段 Nacos 配置。业务代码完全不知道自己在跟 BSC 还是 Bitcoin 说话。
public interface ChainAdapter { ExecutionChain executionChain(); boolean isAddressValid(String address); BigInteger getBalance(String address, String token); List<ChainEvent> scanBlock(long fromBlock, long toBlock); String broadcastSignedTx(byte[] signedTx); Optional<TxReceipt> getReceipt(String txHash); int requiredConfirmations(); }
Dialect comparison
| Dialect | Signing | Address | Confirms | Token model | v1.0 |
|---|---|---|---|---|---|
| EVM (BSC) | ECDSA secp256k1 + low-s + EIP-191 | 0x + 40 hex, checksum-insensitive |
15 | ERC-20 USDT, gasPrice fixed 3–5 gwei | live |
| EVM (ETH / Sepolia) | ECDSA + EIP-1559 (maxFeePerGas + maxPriority) |
same as BSC | 12 | ERC-20 USDT, dynamic fee | config-ready |
| TVM (TRON) | ECDSA over TRX RLP (no EIP-191) | base58check T… |
19 | TRC-20 USDT (no bool return) |
501 (TD-61) |
| BTC | PSBT v0 · BIP-143 P2WPKH sighash · ECDSA | bech32 bc1q… (SegWit-only) |
6 | UTXO model, no token concept | sign ✓ · scan ⚠ (TD-67) |
| LOCAL (Anvil) | ECDSA same as BSC | same as EVM | 1 (instant) | Anvil-deployed test USDT | dev |
Smart contracts
Three UUPS proxies under duobao-contracts/, all owned by a
Gnosis Safe 3-of-5 wired through a 24-hour Timelock.
No upgrade can sneak through; any single signer can cancel mid-window.
| Contract | Responsibility | Key guards |
|---|---|---|
| DuobaoVault | Holds USDT pool per chain. withdraw(user, amount, nonce, sig) for backend-signed exits; payout(winner, amount) for on-chain modes (v1.1). |
nonReentrant, processedNonce[] idempotency, EMERGENCY_ROLE pause |
| DrawCoordinator | Chainlink VRFv2.5 client. requestRandom(roundId) + async fulfillRandomWords callback. (v1.0: deployed but unused.) |
Subscription guard, replay protection |
| AccessRegistry | Central role matrix: OPERATOR · WITHDRAWER · SWEEPER · EMERGENCY. |
Role-grant via Timelock only |
vrf.{coordinatorAddress, keyHash, subscriptionId, minimumRequestConfirmations}
per chain. VrfRequestService exists with a deterministic mock
(SHA-256(executionChain:gameId:requestId)) so non-prod environments can
exercise the callback flow without burning LINK.
Data & event spine数据与事件主干
where state lives状态住在哪里
Twelve isolated schemas, no cross-service joins. Consistency rides on Kafka
topics with at-least-once delivery and consumer-side idempotency keyed
on ref_id. Three retries then DLQ + alert.
十几个互相隔离的 schema,不允许跨服务 join。一致性靠 Kafka topic
的至少一次投递 + 消费侧用 ref_id 做幂等。重试 3 次后进 DLQ 并告警。
Load-bearing tables
wallet_transaction.
∑ debits == ∑ credits; verified by integration test.
ref_id at write time.
Core Kafka topics (selected)
DrawEngine implementation; chain-service if VRF/FULL_CHAIN.
docs/5-contract/events.md. Every consumer is required to be
idempotent on ref_id; 3 retries then DLQ → PagerDuty.
Security posture安全姿态
where the paranoia lives偏执住在哪里Four pillars: KMS custody, JWT with revocation, idempotency at every money seam, and a service-to-service auth model that assumes the internal network is hostile. 四根支柱:KMS 私钥托管、可吊销的 JWT、 每一处资金接缝的幂等、以及"假设内网就是敌对的"的服务间认证模型。
01KMS custodyKMS 私钥托管
- Private keys live in AWS KMS (
ECC_SECP256K1, asymmetric, non-extractable). sign-service callskms:Sign; never holds key material. - EVM signing: KMS returns DER → sign-service decodes, applies low-s canonical form, recovers
v(TD-20). - BTC HD seed encrypted at rest in KMS; decrypted once at
@PostConstruct, derivation runs in-memory, buffer cleared after sign (TD-17). - Operator + hot / warm / cold wallets each in their own KMS key; cold wallet operations route through Gnosis Safe 3-of-5 + 24 h Timelock.
ChainBootValidator&SignBootValidatorrefuse non-localprofile when stub flags are set — fail-closed boot.
02JWT & sessionJWT 与会话
- HS256 HMAC v1.0 (asymmetric RS256 candidate, TD-55). Issuer: user-service. Verifier: gateway-service.
- Access TTL 15 min; refresh TTL 7 days; opaque server-side refresh tokens with replay-detection on
/auth/v1/refresh(TD-53). - jti revocation: gateway checks
jti:${jti}against Redis blacklist on every request; user-service writes on logout / refresh (TD-54). - Telegram
initDataHMAC verified against bot token on every login; bot token in Secrets Manager.
03Idempotency seams幂等接缝
- Deposits: keyed by (txHash, from, to, amount) — natural uniqueness from chain.
- Withdrawals: backend-generated
noncestored inDuobaoVault.processedNonce[]; chain-level guard, can't be replayed even with a leaked signature. - Orders: Feign
X-Idempotency-Keyheader; gateway injects if absent; game-service de-dupes onref_id. - SMS: per-IP / per-60s rate-limit via Redis fixed window.
- Kafka consumers: all keyed on
ref_id; replay-safe by contract.
04Internal auth内部认证
- Service-to-service Feign carries a short-TTL
X-Service-Tokenfrom Nacos; gateway injects when bridging external → internal. - sign-service isolation: accepts only
(keyId, payload). No raw business params reach it. Whitelist of callers (chain, wallet). sign_audittable stores hash of every signing request — full forensic trail if a key is suspected compromised.- mTLS upgrade tracked (TD-19); current short-token model acceptable for the closed VPC.
- Address binding currently TOFU; signed-message challenge planned for v1.1 (TD-26).
../one/ codebase shipped with Fastjson 1.2.62 (CVE-2022-25845 RCE),
predictable block.prevrandao, unverified JWTs, and 21 credentials in git
history. duobao-latest is a from-scratch rewrite — zero shared code.
The legacy repo is a spec to read, not a codebase to fork.
Ops posture运维姿态
deploy & observe部署与观测Four environments, six chain enums, fully orthogonal. Nacos is the config plane, AWS Multi-AZ + MSK is the data plane, Prometheus + business-metric dashboards are the observability plane.
Environment matrix
| Env | Chains active | Stack shape | Purpose |
|---|---|---|---|
| LOCAL | Anvil + BSC testnet | docker-compose · single host · 9 services + MySQL + Redis + Kafka + Nacos + Anvil | Dev workstation. bin/bootstrap.sh one-command (Batch H in progress). |
| TESTNET | Sepolia + BSC testnet + Anvil | 1 instance / service · single AZ · MSK single-broker | QA integration, contract-deploy rehearsal. |
| STAGING | Mainnet (BSC + ETH) optional | Multi-AZ · production scale-down replica | Pre-prod simulation with real chain RPCs (small caps). |
| PRODUCTION | BSC + BTC (v1.0) | EC2 t3.medium · Multi-AZ RDS · ElastiCache Redis cluster · MSK Kafka · Chainlink mainnet (deferred) | Live user traffic. |
Config plane (Nacos, three tiers)
| Tier | What | Reload |
|---|---|---|
| L0 — bootstrap | Service DB endpoint, KMS endpoint, RPC nodes, Telegram bot id. Source: env vars + bootstrap.yml. |
Restart only |
| L1 — runtime | one.draw.mode, prize ratio, per-chain confirms, fee strategy, sweep threshold, operator address. |
Hot — affects next draw |
| L2 — business | Rounds, users, orders, withdrawal queue. Lives in MySQL, not Nacos. | Per-write |
Observability
| Layer | Signals |
|---|---|
| Infra | Micrometer → Prometheus. QPS · p99 latency · error rate per endpoint. CPU / mem / heap per service. |
| Business | Tickets sold/min · GMV · VRF success % (when wired) · withdrawal queue depth · refund rate. |
| On-chain | Hot wallet balance · cold wallet multisig pending count · LINK subscription balance · broadcast nonce holes · failed-tx ratio. |
| Treasury | Daily 4-way reconciliation: wallet ledger ↔ on-chain balance ↔ USDT settlement ↔ admin audit. Drift > 1e-6 USDT pages on-call. |
./gradlew testApp · backend unit + Testcontainers integration.scripts/smoke-e2e.sh · full flow: login → deposit → ticket → draw → withdraw.docker-compose up + bin/bootstrap.sh · local 9-service + Anvil contract deploy.
Production readiness生产就绪度
what's real, what's stub什么是真的、什么是桩Honest matrix. Anything stub returns a safe default (501, empty list, log to stdout); a boot validator refuses prod profile when a stub flag is set. Anything partial works for the core path but has a documented seam. 诚实矩阵。标 stub 的全部返回安全默认值 (501、空列表、写 stdout);启动期校验器在 stub 旗标还在的情况下拒绝以 prod profile 启动。 标 partial 的对核心路径可用,但有明确文档化的缝隙。
| Capability | Status | What's missing | Tech-debt anchor |
|---|---|---|---|
| User auth (TG one-tap login, JWT, TOTP) | prod | — | — |
| Deposits — EVM (BSC, ETH) | prod | — | — |
| Deposits — BTC | partial | Passive scan loop stubbed; user pastes txHash manually. |
TD-24 |
| Deposits — TRON | stub | No adapter; StubChainAdapter returns 501. |
TD-61 |
| Draw — BACKEND (commit-reveal) | prod | — | — |
| Draw — VRF_ONLY / FULL_CHAIN | stub | Chainlink VRFv2.5 integration + DrawCoordinator deployment. | TD-59 |
| Withdrawals — EVM USDT | prod | Multi-sig cold path live; signed nonce idempotency. | — |
| Withdrawals — BTC | partial | sign-service PSBT ✓; chain-service RPC + UTXO selection ⚠. | TD-67 |
| KMS signing — EVM + BTC PSBT | prod | Both closed 2026-05-13. | TD-17, TD-20 |
| Auto-sweep (hot wallet collection) | stub | Job exists, throws FEATURE_NOT_IMPLEMENTED. Manual sweep ok for launch. |
TD-60 |
| Admin business endpoints | partial | P0 subset (draws / RBAC / audit export) ≈ 3 working days to wire. ~70% endpoints DTO-only. | TD-38 |
| SMS dispatch (Aliyun / Twilio / Tencent) | stub | Real SDK adapters; dev logs to stdout. | TD-32 |
| Risk engine | planned | Code shell only. Integration with game / wallet event streams scheduled v1.1. | — |
| Audit trail (append-only) | prod | DB-role enforced INSERT-only; cryptographic hash on every entry. | — |
| Reconciliation (4-way daily) | prod | Wallet ledger ↔ on-chain ↔ USDT settle ↔ admin audit; pages on drift. | — |
| CI/CD pipeline | stub | No .github/workflows/ yet. Manual ./gradlew testApp for now. |
flagged in progress.md |
Top-5 deferred items by impact
| # | Item | Cost | Blocks |
|---|---|---|---|
| 1 | TD-59 — DrawCoordinator + Chainlink VRFv2.5 | 5–7 days | Non-BACKEND draw modes |
| 2 | TD-61 — TRON ChainAdapter (trident-java + TRC20 + base58check) | 3–4 days | TRON deposits / withdrawals |
| 3 | TD-67 — chain-service BTC RPC + UTXO selection + PSBT constructor | 4–5 days | BTC withdrawal — 后续开放 (out of v1.0) |
| 4 | TD-60 — Auto-sweep job (cron + threshold + KMS signer) | 2–3 days | Capital efficiency only — launchable without |
| 5 | TD-38 (P0 subset) — Admin draws / RBAC / audit-export endpoints | 3 days (P0); 6–8 days full | Launch operations |
What's interesting有意思的设计
things to raise an eyebrow at值得挑眉的几点Ten observations a senior engineer is likely to find non-obvious or unusually disciplined for a project at this stage. Order is rough; weight your own. 十条观察,资深工程师在这个阶段大概率会觉得"不显然"或"对这个阶段的项目而言异常自律"。 顺序是大致的,自行赋权。
Environment and chain are independent enums, all the way down环境与链是相互独立的枚举,贯穿到底
Most projects collapse "testnet = Sepolia" and "prod = mainnet." Here, env
and executionChain are orthogonal; STAGING can target BSC mainnet, LOCAL can
target Sepolia. Nacos namespaces isolate per-env, per-chain config blocks isolate
per-chain knobs. Means real capital tests happen in STAGING before launch.
Commit-reveal beats VRF for v1.0 stakes在 v1.0 这个体量下,commit-reveal 优于 VRF
For 1-USDT seats, paying ~200k gas per draw to Chainlink doesn't pencil out. So
v1.0 ships a platform-signed commit (EIP-191) before the round, reveals the seed
after, and anyone can locally verify SHA-256(seed) matches.
Zero gas. Auditable. The trade is operator discipline — leaking the seed early
voids the round. For micro-stakes this is the right call.
Sign-service refuses to know what it's signingsign-service 拒绝知道自己在签什么
sign-service accepts only (keyId, payload) — never business parameters.
chain-service and wallet-service pre-validate, pre-authorize, then send a hashable
blob. sign_audit table records the hash of every signing call. If a key
is suspected leaked, there's a complete forensic trail of what was signed by whom.
Most teams don't go this far.
Schema-level financial isolation, not "logical microservices"在 schema 层做资金隔离,不是"逻辑上的微服务"
Each service has its own MySQL schema with its own credentials and its own migration
ownership. wallet-service literally cannot read one_user.user_info — its DB
user has no grant on that schema. Cross-service consistency is Kafka + idempotency,
not 2PC. Doesn't permit accidental data leaks; does require event-sourcing maturity.
Hand-rolled BIP-84 PSBT — BTC 后续开放手写 BIP-84 PSBT(BTC 后续开放)
BTC deposit/withdrawal is out of v1.0 scope (后续开放) — v1.0 USDT rails
cover BSC / ETH / TRON only. The retained blueprint: when opened, BTC withdrawal will not
use a third-party custody API; sign-service would implement PSBT v0 encoding, BIP-143
P2WPKH sighash, and BIP-84 derivation (m/84'/0'/uid') itself —
non-custodial UX without vendor lock-in, at the cost of more audit surface.
Multi-tier custody with a 24-hour kill window多层托管 + 24 小时熔断窗口
Hot wallet (KMS-signed, small cap). Warm (2-of-3 multisig, medium cap). Cold (Gnosis Safe 3-of-5 + 24h Timelock, large cap). Large withdrawals queue for a full day; any of 5 signers can cancel mid-window. Institutional-grade custody plumbing at a micro-stakes product — that's a product-trust bet, not an engineering reflex.
Append-only audit, enforced at the DB role level仅追加的审计日志,在 DB role 层强制
one_audit and wallet_transaction are INSERT-only at the
PostgreSQL role grant level — not just "we promise not to update." An ORM bug, a
rogue admin script, or a SQL injection cannot UPDATE or DELETE an audit row. Combined
with a cryptographic hash chain, the audit is regulator-grade.
Boot validators fail-closed on stub flags启动校验器在 stub 旗标存在时拒绝启动
ChainBootValidator and SignBootValidator refuse to come up
on a non-local profile when a stub is still wired. You can't accidentally
deploy a build where SMS goes to stdout or VRF returns SHA-256(...).
The stubs are tracked, named, and physically prevented from reaching production.
Contract upgrades route through Timelock, no exceptions合约升级一律走 Timelock,无例外
UUPS proxies, owner is a Gnosis Safe, Safe transactions go through a 24h Timelock. No "we can upgrade in an emergency" backdoor. Storage layout is gap-guarded. Etherscan source verification is a release-checklist mandate, not a wish. The contract security thinking is closer to a DEX than a consumer app.
Documentation is contract, not aspiration文档是契约,不是愿望
Every doc has a Status: LOCKED frontmatter. Changes require an ADR.
Code reviews check against docs. The whole project followed a strict pipeline
(0-feasibility → 1-product → 2-requirements → 3-arch → 5-contract → 6-standards
→ 7-test-plan → 8-sprint → 9-ops) before any application code landed. Rare
at this stage; rarer still that it survived contact with implementation.