Skip to content

Canonical rails: buy staked subnet alpha from MetaMask with USDC - #3110

Open
unarbos wants to merge 2 commits into
mainfrom
swap-demo
Open

Canonical rails: buy staked subnet alpha from MetaMask with USDC#3110
unarbos wants to merge 2 commits into
mainfrom
swap-demo

Conversation

@unarbos

@unarbos unarbos commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Protocol-owned rails from USD on an EVM chain to staked alpha on Bittensor: one ERC-20 per subnet, minted exclusively by hub message and backed 1:1 by alpha in an on-chain escrow. Balances rebase upward as the escrow earns emissions; sell() unwinds back to USDC.
  • Hub side: pallet-usd-psm (PSM assets, protocol-owned tUSD/TAO pool, escrow staking, share-index accounting, sequential envelope nonces, outbound queue + index heartbeat), the 0x814 USD precompile, and rails runtime API/RPC.
  • Spoke side (Solidity): Gateway (trusted-sender envelope door), RailsPortal (buy(), USD release, shared nonce counter), rebasing share token, and EnvelopeLib (SCALE encoding, golden-vector tested against Rust and TypeScript).
  • Local rig: subtensor localnet + anvil fake Base + Hyperlane (self-run agents), just rails-up|rails-demo|rails-ping|rails-down. Creates 8 demo subnets carrying real mainnet identities (name, description, logo) read back from the chain.
  • MetaMask demo page (demo/index.html): subnet dropdown, pool-quoted prices via view precompiles, wallet-free faucet, and self-healing for MetaMask's stale-nonce-after-localnet-reset failure mode. swapdemo.md documents how to run it.

Test plan

  • forge test — 18 contract tests (rebasing, minter windows, nonce guard, golden envelopes)
  • cargo test -p pallet-usd-psm — pallet unit tests (nonce ordering, buy/sell pipelines, fallbacks, heartbeat)
  • pnpm exec moonwall test rails — 12 e2e tests on the live rig (buy → mint, index accrual, attestation consistency, sell → USDC release, nonce sync, failure drills)
  • Manual MetaMask walkthrough: connect, faucet, buy, rebase tick, sell

Made with Cursor

One ERC-20 per subnet on an EVM chain (demo: fake Base on anvil), each
backed 1:1 by staked alpha in a hub escrow on Bittensor. Buys lock USDC,
mint tUSD, swap through a protocol-owned pool, and stake; balances rebase
as emissions accrue; sells unwind back to USDC. Ships pallet-usd-psm, the
USD precompile, Gateway/Portal/share-token contracts, a Hyperlane-based
local rig (8 subnets with real mainnet identities), a MetaMask demo page,
and an e2e suite. See swapdemo.md to run it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
subtensor Error Error Aug 24, 2026 2:50pm

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment on lines +62 to +69
if (kind == SenderKind.UsdPortal && amount > 0) {
// Secure the backing first: canonical USD into the PSM escrow.
// Reverts here (e.g. mint window exhausted) leave funds locked at
// origin and let the relayer retry later.
canonicalUsd.mint(psmEscrow, amount);
}

IUsdRails(IUSD_RAILS_ADDRESS).gatewayExecute(amount, envelope);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] Sender kind is not bound to the envelope action

A trusted UsdPortal may supply an arbitrary envelope through RailsPortal.deposit(), including SellShares. The gateway mints amount of canonical USD and then the runtime treats that same amount as already-burned shares, without proving any caller owned or burned them. An attacker can therefore reduce global outstanding shares, unstake other holders’ backing, and direct the USD release to themselves. Authenticate the sender kind alongside the decoded action and require sell envelopes to originate from the registered token for that exact domain/netuid.

Comment on lines +484 to +485
fn on_idle(now: BlockNumberFor<T>, _remaining_weight: Weight) -> Weight {
Self::flush_outbound().saturating_add(Self::heartbeat(now))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] Unbounded on_idle work can halt block production

This hook discards remaining_weight. OutboundQueue is unbounded, failed dispatches are retained indefinitely, and every queued item is retried in one block; heartbeat also iterates every route. Users can continually enqueue successful buys while a failing outbound route grows the retry set, eventually forcing work beyond the block budget. Bound the queue and process only as many entries/routes as the supplied weight permits, leaving a cursor for later blocks.

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: VULNERABLE

MEDIUM scrutiny: 5-month-old but highly active contributor with repository write permission; disclosed Cursor co-authorship, no Gittensor association found; swap-demo -> main.

All three previously reported critical vulnerabilities remain present at the current head.

Findings

Sev File Finding
CRITICAL runtime/src/lib.rs:238 Runtime changes reuse spec_version 448 (off-diff)
CRITICAL contracts/evm/src/Gateway.sol:69 Sender kind is not bound to the envelope action inline
CRITICAL pallets/usd-psm/src/lib.rs:485 Unbounded on_idle work can halt block production inline

Other findings

  • [CRITICAL] Runtime changes reuse spec_version 448 (runtime/src/lib.rs:238) — This PR adds a pallet, storage, hooks, precompile behavior, and runtime APIs while runtime/src/lib.rs:238 remains spec_version: 448. Nodes carrying the existing spec-448 native runtime may substitute incompatible native code for the upgraded Wasm, causing divergent state transitions or a chain split. Increment spec_version for this runtime upgrade.

Prior-comment reconciliation

  • 492a5136: not addressedruntime/src/lib.rs:238 still sets spec_version: 448.
  • 339334c5: not addressedGateway.handle still forwards the envelope without binding its action to SenderKind.
  • d2b2d561: not addressedon_idle still ignores remaining_weight and invokes both unbounded workloads.

Conclusion

The unchanged settlement authorization permits reserve theft, while the unbounded runtime hook and unchanged runtime version threaten chain liveness and consensus safety.


📜 Previous run (superseded)
Sev File Finding Status
CRITICAL runtime/src/lib.rs:238 Runtime changes reuse spec_version 448 ➡️ Carried forward to current findings
runtime/src/lib.rs:238 still sets spec_version: 448.
CRITICAL contracts/evm/src/Gateway.sol:69 Sender kind is not bound to the envelope action ➡️ Carried forward to current findings
Gateway.handle still forwards the envelope without binding its action to SenderKind.
CRITICAL pallets/usd-psm/src/lib.rs:485 Unbounded on_idle work can halt block production ➡️ Carried forward to current findings
on_idle still ignores remaining_weight and invokes both unbounded workloads.

# 🔍 AI Review — Auditor (domain review) has not yet run on this PR.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

…les.

The .rails registry, agent databases, and CREATE2 deployments outlive a
chain restart from genesis, leaving the rig half-wired. Hyperlane deploy
now checks the mailbox has code before trusting the registry, the forge
deploy scripts skip creation when code already sits at the CREATE2
address, and agents.sh wipes validator/relayer state when the mailbox
set changes. Salt bumped to v2 for a clean local address space.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

canonicalUsd.mint(psmEscrow, amount);
}

IUsdRails(IUSD_RAILS_ADDRESS).gatewayExecute(amount, envelope);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] Sender kind is not bound to the envelope action

kind controls only whether canonical USD is minted; the opaque envelope is then executed without verifying that UsdPortal carries a deposit action or RemoteToken carries SellShares. A trusted portal exposes arbitrary envelope construction through deposit, so a user can submit a SellShares envelope without burning shares and make the hub release escrow. Decode and validate the action against kind before calling the precompile.

/// push the live share index to every routed spoke on a fixed block
/// cadence, so rebasing balances tick without any user action.
fn on_idle(now: BlockNumberFor<T>, _remaining_weight: Weight) -> Weight {
Self::flush_outbound().saturating_add(Self::heartbeat(now))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] Unbounded on_idle work can halt block production

The hook ignores remaining_weight. flush_outbound drains the entire unbounded queue, and heartbeat iterates every route while performing EVM mailbox dispatches. The returned weight is only retrospective; execution can already exceed the block budget and prevent block production. Bound both workloads, stop before exhausting remaining_weight, and persist cursors/retries for later blocks.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant