The USTRY router
Solana USDC into a Stellar-native Treasury bond. The only thing here that has touched mainnet.
USTRY is a tokenised US Treasury bond that lives, in practice, on Stellar: 99.97% of the fund is there and Solana holds about $4,000 of it. So the asset cannot be bought where the users are.
relayer/src/router/ closes that gap: move USDC to Stellar over Circle's CCTP,
buy on Stellar's classic orderbook, hold the position, and reverse the trip on
exit.
This is the only part of the project that has ever run on mainnet. On 2026-08-25 it burned $19 USDC on Solana, attested in 6 seconds, minted on Stellar, and filled 17.7045705 USTRY at 2.5bp over NAV. Round trip cost is about 9bp at $25,000.
Two facts from Stellar's own docs shape everything
- Soroban contracts cannot reach the classic orderbook at all. So the buy is
a classic
pathPaymentStrictSendsigned by an ordinaryGaccount, and the router is an off-chain sequencer rather than an on-chain contract. - A Soroban transaction carries exactly one operation. The CCTP mint and the USTRY purchase therefore cannot be atomic, ever. There is always a window between them, and the whole design has to survive it.
A router that cannot be atomic must be resumable
The burn is irreversible from the instant it lands, so from that moment every
state has exactly one correct next action and none of them is "give up".
relayer/src/router/transfer.ts is that decision as a pure function, testable
without a chain.
| Stage | What is at risk |
|---|---|
quoted | nothing has moved. Abandoning here is free |
burned | USDC is destroyed on Solana and exists nowhere. Only Circle's attestation can bring it back. A 404 here is normal |
attested | recoverable by anyone holding the attestation; submission is idempotent, so retry freely |
minted | USDC is sitting on Stellar, unbought. This is the exposed window |
bought | the bond is held. The cross-chain risk is over |
settled | the Solana side has been credited and the trade is closed |
The exposed window is minted, and it is exposed for seconds. That is
survivable here only because the asset is a Treasury bond whose NAV moves
about 0.0093% a day. On a volatile underlying this design would be wrong.
The function is deliberately total: every stage returns an action, and after
burned nothing returns a terminal failure, because after a burn there is no
such thing as unrecoverable, only not yet recovered.
Three facts about Circle's attestation service
- A 404 is normal. It means the attestation is not ready yet, not that anything failed. Treating it as an error is how a router abandons money that was about to arrive.
- Minting is idempotent. Each attestation carries a nonce usable once, so a duplicate submission reverts rather than double-minting. Retrying is always safe, which means the correct response to almost any ambiguity is to retry.
- Re-attestation has no deadline. As long as the burn transaction still
exists on the source chain,
POST /v2/reattest/{nonce}can be called at any time. A stuck transfer is never lost, only delayed.
The documented rate ceiling is 35 to 40 requests per second, and the client designs to the lower one.
Two counter-intuitive things about the burn
Two details that cost money if missed
message_sent_event_data is a signer, and a fresh keypair for every single
burn. The program stores the outgoing message in an account the caller creates
and pays rent on, at 0.00448224 SOL per transfer.
That keypair must be kept. Reclaiming the rent after Circle's five-day window needs it, and the SOL leaks away permanently if it is thrown out with the transaction.
event_rent_payer is a separate signer from owner, which is a deliberate
gift: a relayer can carry the rent so the user never sees it.
Every PDA the burn needs is derived from seeds read out of the programs' own on-chain IDLs rather than from memory or a blog post, because a wrong PDA here does not fail cleanly. It addresses an account that happens not to exist, and the error arrives with no clue as to which of eighteen accounts was wrong.
Pricing the round trip before anything moves
npx tsx src/router/plan.ts 25000
plan.ts quotes both legs against live Horizon, builds the burn purely to prove
the params are constructible at that size, and signs nothing.
The point is to make the cost legible before any money moves, because the cost is not where people expect it. The bridge is free and the bond is liquid; what you actually pay is the orderbook spread twice, and the thing that actually bites is that the two legs cannot be atomic.
Two decimal systems that do not line up
export const STELLAR_DECIMALS = 7;
export const CCTP_DECIMALS = 6;
Not cosmetic. A burn on the Stellar side debits only through the sixth decimal and leaves the seventh behind, so a router that reconciles to the cent will drift by dust on every exit unless it expects this.
Assets are also carried as code:issuer rather than as a bare code, because a
bare code is not an asset on Stellar. "USDC" names dozens of different tokens
and routing through the wrong one is a real trade against a real book.
What would make this stop
Stated in docs/KEYS.md rather than discovered later.
- Circle can denylist a Solana address from CCTP at the protocol level, and
can freeze a Stellar USDC trustline, since their USDC is
auth_revocable: true. - Etherfuse's USTRY is not revocable today, but its
auth_immutableflag isfalse, so that can change. - The USTRY orderbook is 99.85% one market maker, with $2,781 of realised volume in thirty days. The exit this depends on is one account's decision.
Any of those changing is a reason to wind the position down rather than to route around it.
peard