Small/mid-size swaps no longer have to wait on 3-of-5 for every single
transaction. usdt-bridge can now execute Assets.transfer_approved directly via
the automation key's existing delegated allowance when a deposit/withdrawal
passes all three independent guards:
- max_single_tx (50,000 USDT): any single transaction above this always queues
for manual multisig review, no exceptions.
- auto_pay_daily_cap (200,000 USDT): a rolling 24h budget tracked separately
per leg (wUSDT payouts vs USDT payouts draw on separate on-chain allowances),
not a per-tx ceiling - lets many legitimate sub-cap swaps clear same-day.
- circuit breaker: if >=50% of the daily cap is consumed within 30 minutes,
auto-pay halts entirely until a human runs reset-circuit-breaker - a burst
that fast reads as a compromised key or a bug, not organic demand.
The automation key can only ever move funds via its pre-approved allowance
(never mint/burn/set_team/transfer_ownership - those stay multisig-only), so
its worst case is bounded by whatever allowance + daily cap remain, not
unbounded custody. Withdrawal auto-pay releases real USDT immediately but
defers the corresponding wUSDT burn (Owner/Admin-only, no delegation exists
for it) to a periodic manual multisig batch - tracked via a distinct
auto_paid_burn_pending status so it can't be silently forgotten.
Auto-pay is opt-in at the infra level: AUTOMATION_KEY_MNEMONIC unset means
100% manual behavior, unchanged from before. Bumped pezbridge-bot's
renewal_threshold/topup_amount so the next 3-of-5-approved allowance renewal
scales to the new daily cap.
- show_status/show_balances/show_reserves checked the wrong account:
load_wallet_addresses(&config.seed_path), the old, explicitly-retired
single-operator key, never the multisig custody addresses. These
commands were actively misleading, not just unfinished.
- listen_deposits/listen_withdrawals ran as concurrent tasks sharing one
BridgeDatabase JSON file with no locking and a non-atomic write - two
events landing close together could produce a lost update, and a
mid-write crash left a truncated file that hard-crashed the next
start. Added a process-wide async mutex around every load-mutate-save
critical section, and made save() write-temp-then-rename so a crash
mid-write can never corrupt the file in place. load() now refuses to
silently treat a corrupt file as empty (would erase replay-protection
history) - only a genuinely missing file gets a fresh database.
- The reserve-shortfall circuit breaker - the single most safety-critical
signal this tool can produce - only called a local error!() log macro.
Nobody was notified. Added real Telegram alerting (reusing
PezbridgeBot's exact chats/send pattern rather than a second channel)
for reserve shortfalls, max-single-tx-cap deposits/withdrawals needing
manual review, and every new pending_multisig_approval row.
Also removed ProcessDeposit/MintWusdt/TransferUsdt - dead-but-dangerous
CLI subcommands that still loaded the retired operator key and attempted
direct mint/transfer/burn calls that only happened to fail today because
that key lost asset authority. A latent trap, not a safe no-op by
design; production flow is listen-all -> pending_multisig_approval ->
pwap-web signing, never a single-key path.
cargo check: 0 errors, 0 warnings (both binaries).
Renewing the automation key's spending allowance always needs 3-of-5
multisig approval regardless, so having this bot hold one signer's
(Noter's) private key on a VPS to auto-submit one of those three
signatures was never actually saving a real step - the other two
signers still had to sign manually every time. Once pwap-web shipped
a real multisig operations UI (and now wallet-android's Bridge screen
too), the bot's only remaining job is to detect a low allowance and
notify all 5 signers via Telegram - they each sign with their own
wallet through one of pwap-web, pezbridge-sign.pex.mom, or the wallet
app directly. Noter's mnemonic was removed from the VPS entirely
(shred -u) after this rewrite was deployed and verified.
Also adds gen_automation_key.rs (one-off key generation helper) and
the updated systemd unit with LoadCredential removed.
listen_deposits/listen_withdrawals matched the bridge address by
checking whether a Debug-formatted string of the decoded dynamic Value
contained a 10-char slice of the address's SS58 text - flagged earlier
this session as unverified and provably broken (that same Debug string
was also fed into convert_ss58_prefix, which only parses real SS58
text and could never have succeeded on it).
Added account_id_from_dynamic(), which decodes an AccountId32's actual
32 bytes out of the event field (peeling the single-item composite
wrapper subxt's dynamic API adds, same nesting seen in Tiki storage
this session) and account_bytes_to_ss58() to re-encode them properly.
Both listeners now compare raw bytes against the target address
instead of matching strings, and derive the sender's real SS58 address
for logging and convert_ss58_prefix() instead of a Debug string.
Verified against a live event, not just read: sent a real 1 wUSDT
transfer from Treasury_1 to the multisig and confirmed the decoder
correctly extracted and matched the multisig's AccountId32 bytes from
the resulting Assets.Transferred event.
The single-key custodial model (see prior commit's own doc comment:
"Custodial: A single keypair controls both sides") was replaced with
a 3-of-5 multisig spanning the Serok/SerokiMeclise/Xezinedar/Berdevk/
Noter tiki role-holders (full record in res/validators-tiki.md,
outside this repo). Discovered along the way: wUSDT's ~100M supply
had zero real USDT backing on Polkadot Asset Hub - Treasury_1 (the
actual asset owner, not the seed_path wallet this tool assumed) held
0 USDT/DOT there, with ~99M of the 100M supply sitting undistributed
in Treasury_1's own account. Migrated Pezkuwi Asset Hub custody in one
atomic batch (transfer 1M wUSDT to the multisig, set_team, then
transfer_ownership last) signed by Treasury_1 - tx
0xd5dc550c5cf04f323e5c1d9c13f2056acafc9f56404c657b9743c15ff3a3aacb,
verified on-chain.
listen_deposits/listen_withdrawals no longer sign and submit
mint/burn/transfer - no key this process holds has authority over the
multisig account. They now only detect on-chain events and record
status=pending_multisig_approval with the exact call parameters an
operator needs; actual approval happens through pwap-web's existing
multisig UI (shared/lib/multisig.ts, shared/lib/usdt.ts), not this
relayer. BridgeConfig gained custody_multisig_polkadot/_pezkuwi
fields.
Added pezkuwi-subxt/pezkuwi-subxt-signer alongside the existing
vanilla subxt dependency: vanilla subxt's codegen hardcodes the
`sp_runtime` module path and its default signed-extension handling
produced "bad signature" on submission against this chain's actual
metadata - pezkuwi-subxt (following vendor/pezkuwi-subxt's own
grant_noter_tiki.rs example: PezkuwiConfig, XCM v3, sudo_unchecked_weight)
is what actually signs and submits successfully. Also used to grant
the Xezinedar and Berdevk tiki roles (previously missing signatories
for the multisig) to two validators via a relay-chain sudo XCM Transact,
since the People chain has no local Sudo pallet.
The withdrawal direction (burn wUSDT -> release real USDT) had no
listener at all - nothing watched Pezkuwi for redemption deposits, and
the manual ProcessWithdrawals command was an empty stub. The deposit
listener also had two real bugs: it minted to the bridge's own address
instead of the sender's (a leftover "for demo" placeholder), and it
never checked whether a deposit had already been processed before
minting again.
- Add is_deposit_processed/is_withdrawal_processed idempotency checks,
enforced before every mint/burn/release, not just recorded after.
- Fix listen_deposits() to mint to the sender's actual Pezkuwi address
(same AccountId32, re-encoded under Pezkuwi's SS58 prefix - both
chains share one derivation, so no memo/registration is needed) and
to persist processed deposits to the database, which it never did.
- Add listen_withdrawals(), mirroring the deposit listener: detects
wUSDT sent to bridge custody, burns it, verifies the Polkadot
custody wallet actually holds enough real USDT before releasing
(circuit breaker - an under-collateralized peg must halt loudly,
not silently short-pay), then releases to the sender's Polkadot
address.
- Add max_single_tx cap so one bad/anomalous event can't move an
unbounded amount automatically.
- Add a Reserves command reporting real custody balance vs circulating
wUSDT supply - the exact invariant that, when broken, is what the
wallet's "waiting for liquidity" banner was masking.
- load_keypair() now prefers BRIDGE_SEED_MNEMONIC (meant to be
Vault-injected, matching how the exchange's hot wallets are already
provisioned) over the plaintext seed_path file, which remains only
as a local/dev fallback with a loud warning.
Flagged, not fixed here: the event-matching in both listeners
(Debug-formatted Value string containing a slice of the bridge's SS58
text) is unverified against a live node in this environment and should
be replaced with subxt's typed/codegen API before this handles real
value. Custody is still single-keypair (no multisig/MPC threshold
signing) - that is an infrastructure/key-ceremony decision, not a code
change, and is the highest-priority follow-up.
- Replace all hardcoded wallet mnemonics with env variable reads
- Add comprehensive e2e test suite (tools/e2e-test/)
- Add zagros validator management tools
- Add subxt examples for mainnet operations
- Update CRITICAL_STATE with zagros testnet and mainnet status
- Fix people chain spec ID and chainspec build script
chain-spec-tool:
- CLI utility for chain spec manipulation and validation
- Used for mainnet chain spec generation and verification
usdt-bridge:
- Custodial bridge for wUSDT token operations
- Handles USDT wrapping/unwrapping between external chains and Asset Hub
- Configuration in bridge_config.json