Add USDT0 (LayerZero OFT) to the cross-chain and assets skills - #116
Add USDT0 (LayerZero OFT) to the cross-chain and assets skills#116kaankacar wants to merge 15 commits into
Conversation
Restores layerzero.md, pulled in 631db31 because the rail could not be verified end to end, and anchors it on USDT0 — now live on the mainnet endpoint and the first production OFT on Stellar. Every address and signature re-checked on 2026-08-27: - Endpoint components from the LayerZero metadata API. The testnet endpoint moved since the original file (CBQOTWFU -> CALTBA5S), so the stale value is replaced. - USDT0's classic asset, SAC, OFT, SAC manager and OneSig against Horizon, the ledger, and USDT0's deployments page. - The OFT surface (SendParam, quote_oft, quote_send, send, OFTReceipt) against LayerZero's Stellar crates and a live mainnet send. - The OApp skeleton's import paths and __lz_send argument order against monorepo-external at HEAD. The pathway list from the issue is deliberately not copied in: mainnet traffic already spans more EIDs than it names. The file teaches peer(eid) instead, alongside is_paused, fee_bps and rate_limit_config as live reads. assets gains the other half of the story: USDT0 as the live example of a contract-administered classic asset, why the issuer must be locked for that model to hold, that a missing stellar.toml is not a red flag, and a read-only pre-listing recipe that verifies an asset from the ledger.
|
There was a problem hiding this comment.
Pull request overview
Adds USDT0 and LayerZero guidance across Stellar’s cross-chain and asset skills.
Changes:
- Adds LayerZero/OFT routing, pitfalls, and OApp guidance.
- Adds USDT0 SAC administration and pre-listing checks.
- Updates the site’s cross-chain skill card.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
skills/cross-chain/SKILL.md |
Adds LayerZero and USDT0 routing. |
skills/cross-chain/layerzero.md |
Documents LayerZero, USDT0, OFT, and OApps. |
skills/assets/SKILL.md |
Adds USDT0 administration and validation guidance. |
site/src/data/skills.ts |
Updates cross-chain card metadata. |
Suppressed comments (1)
skills/cross-chain/layerzero.md:126
- This recommends the testnet endpoint as a working rehearsal path, but the required send re-test was not run and the next bullet says sends were previously blocked by
UnsupportedMessageLib. Re-test before merge and make this recommendation conditional or document the current failure if sending is still unsupported.
- **No USDT0 testnet deployment.** USDT0's deployments page lists no Stellar testnet entry (checked 2026-08-27), so a testnet round trip of USDT0 itself is not available. Rehearse instead with LayerZero's own testnet endpoint (EID `40600`) and your own OApp or OFT, and keep mainnet USDT0 work to read-only simulation until the flow is proven.
- **Testnet sends were previously blocked** by `#1213 UnsupportedMessageLib` (the required DVN did not support the endpoint's only registered message library, August 2026). The testnet endpoint has been redeployed since, so re-check current status rather than treating that error as permanent.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| --send_param '{"dst_eid":30101,"to":"<32-byte hex>","amount_ld":"10000000","min_amount_ld":"9950000","extra_options":"","compose_msg":"","oft_cmd":""}' | ||
| ``` | ||
|
|
||
| **Fees are paid in XLM.** `quote_send` returns a `MessagingFee`; `native_fee` is XLM in stroops, and `send` transfers it to the endpoint through the native SAC. A recent mainnet send of `0.5` USDT0 to Arbitrum cost `4864058` stroops (about `0.486` XLM) — quote it, never assume it. `refund_address` receives any excess. |
|
|
||
| Source of truth: [LayerZero-Labs/monorepo-external](https://github.com/LayerZero-Labs/monorepo-external) — the protocol contracts (`contracts/protocol/stellar/`), the OApp packages (`apps/oapp-app/contracts/stellar/`), the OFT and SAC-manager contracts (`apps/oft-app/contracts/stellar/`), and the worked reference at `apps/project-types/omni-counter-app/contracts/stellar/`. There is no Stellar package in the public `LayerZero-v2` repo and no LayerZero Stellar crate on crates.io — work from this monorepo. | ||
|
|
||
| The skeleton below compiles to `wasm32v1-none` with the full receive surface exported, and every import path and argument order in it was re-checked against the monorepo on 2026-08-27. |
| 3. **Classic Stellar recipients need a trustline first.** A `G…` account cannot receive an issued asset (USDC included) without a trustline to that asset. Bridged funds destined for an account without one will not land. Check and provision before starting the transfer — see `../assets/SKILL.md`. | ||
| 4. **Cross-chain is asynchronous.** Every rail has a wait: CCTP waits for finality plus Circle's attestation (seconds to ~15 minutes depending on chain and finality threshold), Axelar waits for validator confirmation, intents wait for a market maker. Build UIs and agents around polling a status, never around "submit and assume". | ||
| 5. **Testnet first, always.** Every rail here except NEAR Intents has a testnet deployment (intents are filled by real market makers — mainnet only; rehearse with dry quotes and a dust-sized swap instead). Do the full round-trip on testnet before touching mainnet — cross-chain mistakes are frequently unrecoverable by design (burns are final, and some misencodings permanently strand funds). | ||
| 5. **Testnet first, always.** Do the full round-trip on testnet before touching mainnet — cross-chain mistakes are frequently unrecoverable by design (burns are final, and some misencodings permanently strand funds). Two rails cannot be rehearsed that way: NEAR Intents is filled by real market makers, and **USDT0 publishes no testnet deployment** even though LayerZero's testnet endpoint exists. For both, the rehearsal path is read-only quotes (`quote_oft` and `quote_send` for USDT0, dry quotes for intents) followed by a dust-sized real transfer — and for LayerZero specifically, you can still exercise your own OApp or OFT on testnet EID `40600` before trusting the mainnet path. |
| - If the asset is bridged, read the bridge contract too — for a LayerZero | ||
| OFT: `token`, `shared_decimals`, `oft_type`, `endpoint`, and `is_paused`. | ||
| See `../cross-chain/layerzero.md`. |
Remove the observed mainnet fee amount. The issue keeps fee numbers out of the file on purpose, and a single quote anchors estimates. Say what was actually checked against monorepo-external at HEAD (import paths, argument order, trait signatures) instead of claiming the skeleton compiles. The compile is the reader's step. Make the testnet rehearsal conditional. Testnet sends failed with #1213 UnsupportedMessageLib in August 2026 and no successful send is recorded since the endpoint was redeployed. Add the read-only OFT invocations to the pre-listing recipe, so step 5 runs instead of only naming the methods. List all three cross-chain companion files in the README tree.
|
🤖 Automated message from Kaan's Automated Triage Bot. Copilot reviewed this PR. All four points were fair. Fixed in
I also re-verified the facts independently today, at this head:
Two checks still need you, @kaankacar:
I am not merging this. The file teaches transaction signing, so a human merges it. I will keep tracking this PR. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Suppressed comments (1)
skills/assets/SKILL.md:544
oft_type()does not tell you every party that can mint;MintBurn(<address>)only identifies the mintable adapter the OFT calls. For the role-gated SAC manager, the owner/authorizer and role admins can changeMINTER_ROLE, and its members are independently enumerable. Stopping at this result leaves the pre-listing check unable to answer its stated trust question.
- **Step 5 tells you who can mint.** `oft_type` returning `MintBurn(<address>)`
means that address mints on credit, so it must be the SAC admin from step 4
or a role holder on it. A `shared_decimals` below the SAC's 7 also means
sends drop the extra digits. See `../cross-chain/layerzero.md` for that rail.
| - Send `1.0000001` USDT0 (`amount_ld` = `10000001`): `amount_sent_ld` is `10000000` and the trailing `0.0000001` **stays in your account**. It is not lost, but it is not sent either. | ||
| - Quote first and compare. `quote_oft` returns an `OFTReceipt` whose `amount_sent_ld` and `amount_received_ld` are already dust-adjusted — treat those, not your input, as the truth. |
| --send_param '{"dst_eid":30101,"to":"<32-byte hex>","amount_ld":"10000000","min_amount_ld":"9950000","extra_options":"","compose_msg":"","oft_cmd":""}' | ||
| ``` | ||
|
|
||
| **Fees are paid in XLM.** `quote_send` returns a `MessagingFee`; `native_fee` is XLM in stroops, and `send` transfers it to the endpoint through the native SAC. The fee tracks the destination route, the DVN set, and executor pricing, so quote every send and never reuse a number from a previous one. `refund_address` receives any excess. |
|
|
||
| Source of truth: [LayerZero-Labs/monorepo-external](https://github.com/LayerZero-Labs/monorepo-external) — the protocol contracts (`contracts/protocol/stellar/`), the OApp packages (`apps/oapp-app/contracts/stellar/`), the OFT and SAC-manager contracts (`apps/oft-app/contracts/stellar/`), and the worked reference at `apps/project-types/omni-counter-app/contracts/stellar/`. There is no Stellar package in the public `LayerZero-v2` repo and no LayerZero Stellar crate on crates.io — work from this monorepo. | ||
|
|
||
| Every import path, argument order, and trait signature below was checked against the monorepo at HEAD on 2026-08-27. It is a skeleton, not a compiled artifact: build it yourself for `wasm32v1-none` before you trust it, and confirm `lz_receive` appears in the exported interface. |
| # Look for: signers all weight 0 (locked), auth_revocable, | ||
| # auth_clawback_enabled, auth_immutable, and whether home_domain exists. |
Copilot's second pass found three real defects. The dust example only held while the route charged no OFT fee. The Stellar OFT keeps the remainder with the sender when effective_fee_bps is 0, and debits the full amount_ld into the fee when it is not, so the example is now conditional. "Fees are paid in XLM" covered only the LayerZero messaging fee. The OFT fee is separate and denominated in the token, so a sender can need both. The pre-listing recipe read the signer list to decide whether an issuer is locked. The ledger entry keeps the master key weight in thresholds and never lists it under signers, so that test passed an issuer with a live master key. Also pin the monorepo revision and toolchain behind the OApp skeleton, and stop claiming oft_type enumerates every minter.
|
🤖 Automated message from Kaan's Automated Triage Bot. Copilot reviewed this PR again. Three of the points were real defects. Fixed in
Two smaller fixes in the same commit:
Sources: I did not compile the skeleton. This sandbox has no Rust toolchain, so I recorded the pinned revision instead of a build result. That compile and one delivered testnet send still need you, @kaankacar. I am not merging this. The file teaches transaction signing, so a human merges it. I will keep tracking the PR. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
skills/cross-chain/layerzero.md:131
- The required testnet check remains unresolved: this status describes the failure before the endpoint redeployment and explicitly records no successful send afterward. The PR description says not to merge until the redeployed endpoint is retested. Run that send and update this note with the current result.
- **A testnet rehearsal is not guaranteed.** You can deploy your own OApp or OFT against the testnet endpoint (EID `40600`), but testnet sends failed with `#1213 UnsupportedMessageLib` in August 2026 — the required DVN did not support the endpoint's only registered message library. That endpoint has been redeployed since, and this file does not record a successful testnet send after it. Send one small testnet message and confirm delivery before you treat testnet as a rehearsal path.
skills/cross-chain/layerzero.md:141
- This confirms that the restored OApp skeleton has not passed the mandatory compile check from the PR description. Signature inspection cannot validate macro expansion or the exported interface. Compile this exact example for
wasm32v1-noneand verify thatlz_receiveis exported before publishing it as a copyable pattern.
It is a skeleton, not a compiled artifact. Signature comparison does not catch macro expansion, feature, dependency, or target errors, and no build was run against this snippet. Compile it yourself for `wasm32v1-none` before you trust it, and confirm `lz_receive` appears in the exported interface.
skills/assets/SKILL.md:619
- This omits the issuer-lock check and therefore contradicts the pre-listing guidance above (
skills/assets/SKILL.md:549-550). A contract SAC admin does not neutralize an unlocked classic issuer: the issuer can still mint, freeze, or claw back directly. Require both verification of the issuer lock and inspection of the contract's role holders.
- **When `AUTH_REVOCABLE` and `AUTH_CLAWBACK_ENABLED` are both set, find out
who holds the admin role before listing the asset.** Those flags mean
balances can be frozen or clawed back, and if the SAC admin is a contract
the real authority is whoever holds its roles — not the issuer account
| | Move **native USDC** between Stellar and an EVM chain or Solana (no wrapped assets, no liquidity pools) | Circle CCTP V2 | [cctp.md](cctp.md) | | ||
| | Have a Stellar contract **call a contract on another chain**, or receive calls from one (arbitrary payloads) | Axelar GMP | [axelar.md](axelar.md) | | ||
| | Make a token — new or an existing Stellar asset — **exist on multiple chains** | Axelar ITS | [axelar.md](axelar.md) | | ||
| | Move **native USDT** between Stellar and an EVM chain | USDT0, a LayerZero OFT | [layerzero.md](layerzero.md#usdt0-native-usdt-on-stellar) | |
| ```bash | ||
| # Read-only. --send=no simulates and never signs or submits. | ||
| stellar contract invoke --id CBOWOLFSDM5PZXNFIVDMP5NZ7U2GSIHED6H6R446QOHF266XINKUMMF6 \ | ||
| --source-account alice --network mainnet --send=no \ | ||
| -- quote_send --from <G_SENDER> --pay_in_zro false \ | ||
| --send_param '{"dst_eid":30101,"to":"<32-byte hex>","amount_ld":"10000000","min_amount_ld":"9950000","extra_options":"","compose_msg":"","oft_cmd":""}' |
Copilot's third pass caught two gaps against the repo's own rules. The contributing guide asks for a matching scenario under evals/ when a change touches what a skill teaches, and cross-chain had no scenarios at all. Add three for the USDT0 rail (inbound routing, the dust question, the testnet rehearsal) and one cross-skill scenario for the collateral question, then correct both READMEs, which still described cross-chain as uncovered. The outbound section showed a command shape for quote_send only. Add quote_oft and send, all three simulated with --send=no, and replace the <G_SENDER> placeholder: inside a bash fence that is a stdin redirect, not an argument. Also require the issuer-lock check alongside the role check in the security list. A contract admin does not contain an issuer that can still sign.
|
🤖 Automated message from Kaan's Automated Triage Bot. Copilot reviewed the fix commit. Two more points were correct. Fixed in
One suppressed point was fair too. The security list asked for the admin roles but not the issuer lock. A contract admin does not contain an issuer that can still sign. Both checks are now required. I rejected two suppressed points. They ask for the testnet send and the Rust compile. Those are the two checks this sandbox cannot run, and they are already with you. I also merged The merge is still yours, @kaankacar. The file teaches transaction signing. I will keep tracking this PR. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Suppressed comments (2)
skills/cross-chain/layerzero.md:58
- This repeats the account-only trustline requirement as if it applied to every recipient. Contract (
C…) recipients can hold SAC balances without trustlines, so condition this step on the destination being aG…account.
1. The recipient's trustline must exist first (rule 1 above).
skills/cross-chain/layerzero.md:141
- The PR's required compile verification remains unfinished here. Signature inspection cannot catch macro expansion, feature, dependency, target, or exported-interface failures, and the PR description explicitly says not to merge without this check. Compile this exact skeleton against the pinned revision for
wasm32v1-none, confirmlz_receiveis exported, and update the snippet or verification note with the result.
Storage keys mirror these names (`Peer(eid)`, `FeeBps(eid)`, `RateLimit(direction, eid)`, `EnforcedOptions(eid, msg_type)`), so `stellar ledger entry fetch contract-data --contract "$OFT" --key-xdr "$KEY"` reads them without any invocation at all.
|
|
||
| ## USDT0: native USDT on Stellar | ||
|
|
||
| USDT0 is USDT moved by burn-and-mint over LayerZero's OFT standard. There is no wrapped token and no pool: the OFT burns on the source chain and mints on the destination. On Stellar it is a **classic asset** with a locked issuer whose SAC admin is a contract. |
|
|
||
| ### The rules that save funds | ||
|
|
||
| 1. **The recipient needs a USDT0 trustline before anything inbound lands.** A `G…` account cannot hold an issued asset without one. This is the single most common inbound failure. |
| | **Swap any asset cross-chain** (BTC, ETH, SOL, … → XLM or Stellar USDC) without integrating a bridge yourself | NEAR Intents | [below](#near-intents-intent-based-swaps) | | ||
|
|
||
| Rules of thumb: if the asset is USDC and both ends are CCTP chains, CCTP is the cheapest and most direct (it burns and mints Circle-native USDC — nothing wrapped, nothing pooled). If you need logic, not just value, on the far chain, that is message passing — Axelar GMP is the rail for it. If you control a token and want it multichain, that is ITS. If the user just wants "turn my X on chain A into Y on Stellar" and you don't want bridge plumbing at all, quote it through NEAR Intents. | ||
| Rules of thumb: if the asset is USDC and both ends are CCTP chains, CCTP is the cheapest and most direct (it burns and mints Circle-native USDC — nothing wrapped, nothing pooled). If the asset is USDT, the answer is USDT0 over LayerZero OFT — same burn-and-mint shape, different rail. If you need logic, not just value, on the far chain, that is message passing — two rails do it, and the [comparison at the end of layerzero.md](layerzero.md#choosing-between-axelar-gmp-and-layerzero-oapp) helps you pick between Axelar's shared validator security and LayerZero's app-configured DVN sets. If you control a token and want it multichain, that is ITS or OFT on the same split. If the user just wants "turn my X on chain A into Y on Stellar" and you don't want bridge plumbing at all, quote it through NEAR Intents. |
| ### Limitations and status notes | ||
|
|
||
| - **No USDT0 testnet deployment.** USDT0's deployments page lists no Stellar testnet entry (checked 2026-08-27), so a testnet round trip of USDT0 itself is not available. Keep mainnet USDT0 work to read-only simulation until the flow is proven. | ||
| - **A testnet rehearsal is not guaranteed.** You can deploy your own OApp or OFT against the testnet endpoint (EID `40600`), but testnet sends failed with `#1213 UnsupportedMessageLib` in August 2026 — the required DVN did not support the endpoint's only registered message library. That endpoint has been redeployed since, and this file does not record a successful testnet send after it. Send one small testnet message and confirm delivery before you treat testnet as a rehearsal path. |
The Ethereum leg is an OFT Adapter over canonical USDT, so it locks and unlocks rather than burning and minting. Contract recipients hold SAC balances in contract storage and need no trustline.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
skills/assets/SKILL.md:493
stellar ledger entry fetch accountserializes the XDRflagsfield as a numericu32; it does not emit the named booleans listed here. As written, the pre-listing recipe does not tell readers how to determine the required authorization risks. Document the bit values (USDT0 should yield10) so the check is reproducible.
# 1. Is the issuer locked, and which flags are set?
# Read: thresholds and signers (see "Step 1" below — the signer list
# alone does not answer this), auth_revocable, auth_clawback_enabled,
# auth_immutable, and whether home_domain exists.
stellar ledger entry fetch account --account $ISSUER --network mainnet
skills/cross-chain/layerzero.md:152
- The second explicit merge gate is still outstanding: the issue thread confirms no post-redeployment testnet send was run. Because this endpoint moved and the previous deployment failed with
UnsupportedMessageLib, retain the warning but do not merge until a signed test message is delivered (or the current failure is reproduced) and the dated result is recorded here.
| ### Inbound: EVM → Stellar | ||
|
|
||
| 1. The recipient's trustline must exist first (rule 1 above). | ||
| 2. Send on the source chain against USDT0's OFT there, with Stellar's EID `30600` and the recipient encoded as a 32-byte value. |
|
|
||
| ### Limitations and status notes | ||
|
|
||
| - **No USDT0 testnet deployment.** USDT0's deployments page lists no Stellar testnet entry (checked 2026-08-27), so a testnet round trip of USDT0 itself is not available. Keep mainnet USDT0 work to read-only simulation until the flow is proven. |
| - **Step 5 names the bridge's minter, not every minter.** `oft_type` returning | ||
| `MintBurn(<address>)` means that address mints on credit, so it must be the | ||
| SAC admin from step 4 or a role holder on it. It does not enumerate the | ||
| others: on LayerZero's SAC manager the owner grants and revokes | ||
| `MINTER_ROLE`, so finish the trust question by listing the role holders and |
|
|
||
| Every import path, argument order, and trait signature below was read out of that monorepo at commit `3f1cf3adadca88aa7a4ee5a7ee251c8b7fefcf2f` (2026-08-26), whose `rust-toolchain.toml` pins Rust `1.90.0` and the `wasm32v1-none` target. Pin the same revision when you copy it, because these crates are not versioned on crates.io. | ||
|
|
||
| It is a skeleton, not a compiled artifact. Signature comparison does not catch macro expansion, feature, dependency, or target errors, and no build was run against this snippet. Compile it yourself for `wasm32v1-none` before you trust it, and confirm `lz_receive` appears in the exported interface. |
|
🤖 Automated message from Kaan's Automated Triage Bot. Copilot reviewed the last commit. Two points were real defects. Fixed in
Sources for both:
The evals moved with the teaching. Scenario I rejected the repeated testnet-send point. That check needs signing keys, which this sandbox does not have. It is already with you. The build is green at The merge is still yours, @kaankacar. The file teaches transaction signing. I will keep tracking this PR. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
skills/cross-chain/layerzero.md:221
- This is only true when
pay_in_zrois false. The documented API exposes ZRO payment, and__lz_sendtransferszro_feethrough the endpoint's configured ZRO token; presenting every OApp fee as native-token-only can make callers mishandle a ZRO quote.
- **Fees are quoted, then paid in the chain's native token** (XLM on Stellar). Quote with `__quote` into a `MessagingFee` and pass it to `__lz_send`; underquoting fails the send.
skills/assets/SKILL.md:537
- Stellar multisig authorization sums signer weights. Checking whether one remaining signer reaches a threshold can classify an account as locked even when several signers collectively authorize payments or
SetOptions; require that no combination can meet the medium or high threshold.
signers has an empty `signers` list. Locked means the master weight is `0`
*and* no remaining signer can reach the medium or high threshold. USDT0's
issuer reads `thresholds` `00000000` with no extra signers. (Horizon differs:
| > One hard prerequisite: **lock the issuer** (master key weight `0`) before | ||
| > handing administration to a contract. Payments from a classic issuer are | ||
| > minting, so an unlocked issuer can mint outside the contract and bypass | ||
| > the role model entirely. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
skills/cross-chain/layerzero.md:164
- Issue #115 requires the restored OApp skeleton to be compiled for
wasm32v1-none, but this line confirms that verification has not been performed. Signature comparison cannot catch macro expansion or exported-interface failures, so please compile the pinned snippet and record the verified revision/result before merging.
It is a skeleton, not a compiled artifact. Signature comparison does not catch macro expansion, feature, dependency, or target errors, and no build was run against this snippet. Compile it yourself for `wasm32v1-none` before you trust it, and confirm `lz_receive` appears in the exported interface.
skills/cross-chain/layerzero.md:154
- Issue #115 explicitly requires re-testing a testnet send after the endpoint redeployment, while this status still relies on the pre-redeployment failure and says no current send was confirmed. Re-run a small message against EID 40600 and update this note with the dated result before merge; otherwise the documented operational status may already be stale.
- **A testnet rehearsal is not guaranteed.** You can deploy your own OApp or OFT against the testnet endpoint (EID `40600`), but testnet sends failed with `#1213 UnsupportedMessageLib` in August 2026 — the required DVN did not support the endpoint's only registered message library. That endpoint has been redeployed since, and this file does not record a successful testnet send after it. Send one small testnet message and confirm delivery before you treat testnet as a rehearsal path.
skills/assets/SKILL.md:564
- Checking only direct role holders and the owner misses indirect privilege. LayerZero's RBAC also lets any holder of the role returned by
get_role_admin(role)grant or revoke that role, so a collateral review must enumerate those admin-role holders as well; otherwise an account able to grant itselfMINTER_ROLEis omitted.
others: on LayerZero's SAC manager the owner grants and revokes
`MINTER_ROLE`, so finish the trust question by listing the role holders and
| - The `#[oapp]` macro generates the public surface (`OAppCore`, sender internals, the `lz_receive` entrypoint, options handling). The generated `lz_receive` does peer validation and `endpoint.clear()` **before** dispatching to your `__lz_receive` — don't reimplement either. | ||
| - **`custom = [receiver]` is a footgun.** Passing `#[oapp(custom = [receiver])]` tells the macro to *skip* generating the receiver surface; unless you then supply your own `#[contract_impl(contracttrait)] impl OAppReceiver` (as the counter example does, to customize `next_nonce`), the contract **compiles cleanly but exports no `lz_receive` at all** — an OApp that silently cannot receive. Use plain `#[oapp]` unless you're deliberately taking that surface over. | ||
| - **Peers must be set on both sides.** `set_peer(&dst_eid, &Some(remote_oapp_bytes32), &caller)` on Stellar, and the mirror call on the destination OApp. A message from an unset peer never reaches `__lz_receive`. | ||
| - **Fees are quoted, then paid — in XLM, or in ZRO.** Quote with `__quote(dst_eid, message, options, pay_in_zro)` into a `MessagingFee { native_fee, zro_fee }` and pass that value to `__lz_send`; underquoting fails the send. `__lz_send` pays ZRO whenever `fee.zro_fee` is not `0`. The endpoint rejects that payment unless a ZRO token is set on it (`zro()` → `Option<Address>`, error `ZroUnavailable`). Pass `pay_in_zro = false` unless you read a ZRO token on the endpoint you use. |
| "expected_behavior": [ | ||
| "Quotes with `__quote` and passes the returned `MessagingFee` to `__lz_send`, rather than guessing an amount", | ||
| "Names both fee fields: `native_fee` in XLM and `zro_fee` in the ZRO token", | ||
| "Says `__lz_send` pays ZRO whenever `zro_fee` is not 0, and that the endpoint fails with `ZroUnavailable` when no ZRO token is set on it", |
__quote with pay_in_zro = true reaches the endpoint and panics EndpointError::ZroUnavailable. __lz_send never gets that far: __pay_zro runs first and panics OAppError::ZroTokenUnavailable. Naming one error for both makes a correct handler look wrong. The inbound recipe said "a 32-byte value", which is ambiguous on the one step that loses funds. Spell out the decode: the Ed25519 public key for a G address, the contract ID hash for a C address, no version byte and no checksum. Point away from the CCTP hook-data pattern, which is the opposite shape. Also finish the pre-listing recipe. Step 5 told the reader to list the admin contract's role holders without showing how. Add step 6 with the read-only RBAC calls, and record that the owner and any role-admin holder can grant a role, so an empty role is not a safe role. Align the testnet note with the router and the rehearsal scenario: the proving step is a dust-sized mainnet transfer, not an open condition.
An M strkey decodes to 40 bytes and the OFT reads 32, so the muxed id is dropped and a custodian loses the sub-account it routes on. Say that, give the two workarounds, and note that CCTP hook data takes M directly so the two rails are not confused.
|
🤖 Automated message from Kaan's Automated Triage Bot. Copilot's last review found two real defects. Reading back through the thread, I also found three earlier points I never answered. All six fixes are in The two new ones:
The three I had missed:
Sources: I rejected the repeated compile and testnet-send points again. This sandbox has no Rust toolchain and no signing keys. The build is green at Two checks still need you, @kaankacar:
The merge is still yours. The file teaches transaction signing. I will keep tracking this PR. |
| USDT0 on 2026-08-28: `get_existing_roles` returns `MINTER_ROLE` only, its one | ||
| member is the OFT, it has no admin role, and the owner is the OneSig contract | ||
| `CBCZ5CET…`. So the OneSig signers are the real authority over minting, | ||
| clawback and blacklisting. Re-read it — this is live state. |
|
|
||
| ### The rules that save funds | ||
|
|
||
| 1. **An account (`G…`) recipient needs a USDT0 trustline before anything inbound lands.** A `G…` account cannot hold an issued asset without one. This is the single most common inbound failure. A contract (`C…`) recipient needs none: SAC balances for contracts live in contract storage, not in a trustline. The OFT decides which one you get from the 32-byte recipient — it resolves to a contract address when a contract with that ID exists, and to a `G…` account otherwise. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
skills/assets/SKILL.md:550
get_existing_rolesreturns only roles with at least one member, so iterating that list skips empty privileged roles. Those roles may still have an admin role whose members can grant them later; for example, this recipe never checks the admin of an emptyCLAWBACK_ROLEorADMIN_MANAGER_ROLE. Query all four SAC-manager roles explicitly, then enumerate each role's members and admin role.
# Then, for every role that list returns:
ROLE=MINTER_ROLE
stellar contract invoke --id $MANAGER --source-account alice \
--network mainnet --send=no -- get_role_admin --role $ROLE
stellar contract invoke --id $MANAGER --source-account alice \
--network mainnet --send=no -- get_role_member_count --role $ROLE
stellar contract invoke --id $MANAGER --source-account alice \
--network mainnet --send=no -- get_role_member --role $ROLE --index 0
skills/cross-chain/layerzero.md:184
- The issue's verification plan requires this skeleton to compile for
wasm32v1-nonebefore shipping, and the PR description explicitly asks not to merge without that check. Leaving the snippet knowingly uncompiled does not satisfy that acceptance criterion; run the pinned workspace build and update this status only after the exportedlz_receiveis verified.
It is a skeleton, not a compiled artifact. Signature comparison does not catch macro expansion, feature, dependency, or target errors, and no build was run against this snippet. Compile it yourself for `wasm32v1-none` before you trust it, and confirm `lz_receive` appears in the exported interface.
skills/cross-chain/layerzero.md:174
- This status note still describes a failure observed before the testnet endpoint was redeployed. The issue's verification plan and PR description require a new signed send against the current
CALTBA5S…endpoint before merge; record that result and date, retaining#1213only if it still reproduces.
- **A testnet rehearsal is not guaranteed.** You can deploy your own OApp or OFT against the testnet endpoint (EID `40600`), but testnet sends failed with `#1213 UnsupportedMessageLib` in August 2026 — the required DVN did not support the endpoint's only registered message library. That endpoint has been redeployed since, and this file does not record a successful testnet send after it. Send one small testnet message and confirm delivery before you treat testnet as a rehearsal path.
| "Tests the issuer lock by the master key weight (the first byte of the account entry's thresholds), not by the signer list alone, and treats the extra signers' combined weight against the thresholds", | ||
| "Reports auth_revocable and auth_clawback_enabled as risks to model: balances can be frozen or clawed back", | ||
| "Derives the SAC from the asset rather than trusting a published address, and does not disqualify the asset for having no stellar.toml", | ||
| "Goes past oft_type's MintBurn address and enumerates the SAC admin contract's authority: get_existing_roles, then get_role_member for each role, plus get_role_admin and owner", |
| SENDER=$(stellar keys address alice) # the account that pays and signs | ||
| EVM_TO=0x1234...abcd # the EVM recipient, 20-byte hex | ||
| TO=000000000000000000000000${EVM_TO#0x} # left-padded to 32 bytes | ||
| PARAM='{"dst_eid":30101,"to":"'"$TO"'","amount_ld":"10000000","min_amount_ld":"9950000","extra_options":"","compose_msg":"","oft_cmd":""}' |
A C… recipient only skips the trustline while that contract exists. If it is not deployed at delivery, resolve_address reads the same 32 bytes as an Ed25519 account, and that account can never sign a changeTrust. The pre-listing recipe stopped at 'the owner is a OneSig contract', which names a governance layer rather than an authority. Step 7 reads the owner's own quorum, and says to mark ultimate control unresolved when it cannot be read.
Both quotes call __debit_view, which asserts amount_received_ld >= min_amount_ld. A discovery quote carrying a real floor therefore panics SlippageExceeded on any route that costs more, and hides the receipt. get_existing_roles returns only roles that have a member today. A role's admin role is stored separately and outlives an empty role, so iterating that list alone misses whoever can grant CLAWBACK_ROLE.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
skills/assets/SKILL.md:500
ledger entry fetch accountemits the XDRflagsfield as a numericu32, not these named booleans. As written, the recipe does not explain how to determine the authorization risks it requires. Document the bitmask values (USDT0's10meansAUTH_REVOCABLE | AUTH_CLAWBACK_ENABLED).
# Read: thresholds and signers (see "Step 1" below — the signer list
# alone does not answer this), auth_revocable, auth_clawback_enabled,
# auth_immutable, and whether home_domain exists.
stellar ledger entry fetch account --account $ISSUER --network mainnet
evals/scenarios/routing/04-usdt0-collateral.json:13
- This evaluation codifies the incomplete
get_existing_rolesworkflow: it cannot discover an admin assignment on an empty privileged role. Require explicit checks of all four SAC-manager roles and the members of each configured admin role, or the eval can pass while omitting an account able to grant itself sensitive authority.
"Goes past oft_type's MintBurn address and enumerates the SAC admin contract's authority: get_existing_roles, then get_role_member for each role, plus get_role_admin and owner",
"Does not treat get_existing_roles as the role list: it returns only roles with a member today, so the answer queries MINTER_ROLE, CLAWBACK_ROLE, BLACKLISTER_ROLE and ADMIN_MANAGER_ROLE by name and reports each one's admin role, because an empty role's admin can still grant it",
skills/assets/SKILL.md:546
get_existing_rolesreturns only roles that currently have members, so this loop skips empty privileged roles. An omitted role can still have a configured admin role whose members may grant it, causing the pre-listing check to miss indirect mint/clawback/admin authority. Query all four SAC-manager roles explicitly and enumerate each returned admin role too.
# get_existing_roles lists only roles that have a member today. Ask about
# the empty ones by name as well, and add anything new the call returned.
# Both views below are safe on an empty role: None, and 0.
for ROLE in MINTER_ROLE CLAWBACK_ROLE BLACKLISTER_ROLE ADMIN_MANAGER_ROLE; do
The raw account entry carries flags as a u32 bitmask, not the four named booleans Horizon returns, so the recipe now decodes it: USDT0's issuer reads 10. An admin role's members can grant the role they administer, so they belong in the answer next to the role's own members.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Suppressed comments (4)
Previously missed (1) — in code that hasn't changed since the last review.
evals/scenarios/routing/04-usdt0-collateral.json:4
- This scenario expects
assetsto load, but that skill’s frontmatter still advertises only issuance, trustlines, and SAC bridging—not listing, due diligence, collateral, or asset-risk review. The exact collateral query can therefore route only tocross-chain, leaving the new pre-listing workflow undiscovered. Add these new intents to theassetsfrontmatter description so the trigger expectation is supported.
"skills": [
"assets",
"cross-chain"
skills/cross-chain/layerzero.md:206
- The PR’s required verification explicitly says not to merge until this skeleton builds for
wasm32v1-noneand exportslz_receive, but this note confirms that check was not run. Build this exact snippet against the pinned revision and record the result before merging; signature comparison does not verify macro expansion or target compatibility.
It is a skeleton, not a compiled artifact. Signature comparison does not catch macro expansion, feature, dependency, or target errors, and no build was run against this snippet. Compile it yourself for `wasm32v1-none` before you trust it, and confirm `lz_receive` appears in the exported interface.
skills/cross-chain/layerzero.md:196
- The PR description identifies a post-redeployment testnet send as the second mandatory pre-merge check, but this still records only the earlier failure and no result against the current endpoint. Re-run one testnet delivery and update this status with either the successful transaction or the currently reproduced failure before merging.
- **A testnet rehearsal is not guaranteed.** You can deploy your own OApp or OFT against the testnet endpoint (EID `40600`), but testnet sends failed with `#1213 UnsupportedMessageLib` in August 2026 — the required DVN did not support the endpoint's only registered message library. That endpoint has been redeployed since, and this file does not record a successful testnet send after it. Send one small testnet message and confirm delivery before you treat testnet as a rehearsal path.
skills/assets/SKILL.md:571
- This promises an ABI-free read of both values, but the command only fetches the instance entry containing
Threshold; it never fetches the separate persistentSignersentry. As written, a reader cannot perform the advertised signer verification without the ABI call above.
stellar contract invoke --id $ONESIG --source-account alice \
--network mainnet --send=no -- get_signers # 20-byte secp256k1 addresses
stellar contract invoke --id $ONESIG --source-account alice \
--network mainnet --send=no -- threshold # signatures needed to act
| --output json-formatted --network mainnet | ||
| ``` | ||
|
|
||
| Existence is read when the message is delivered, not when you send it. Re-check right before the send, and never point a route at a contract you have not deployed yet. |
|
|
||
| # 2. Show the user amount_received_ld, get their floor, then rebuild the | ||
| # parameter with it. This is the value you send with. | ||
| read -r MIN_AMOUNT # the minimum the user accepts, in stroops |
| # 3. What does the message cost? Returns MessagingFee { native_fee, zro_fee }. | ||
| stellar contract invoke --id "$OFT" --source-account alice \ | ||
| --network mainnet --send=no \ | ||
| -- quote_send --from "$SENDER" --pay_in_zro false --send_param "$PARAM" | ||
|
|
||
| # 4. The send itself. NATIVE_FEE is the stroop figure step 3 returned; quote | ||
| # it every time. Drop --send=no only when the user agreed to sign. | ||
| read -r NATIVE_FEE # paste the native_fee from step 3 |
| ### Pre-Listing Check (read-only) | ||
|
|
||
| Before you list an asset, display it, or accept it as collateral, answer seven | ||
| questions from the ledger itself. Everything below **simulates only** — | ||
| `--send=no` never signs or submits, and no step needs a key. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (5)
Previously missed (1) — in code that hasn't changed since the last review.
skills/assets/SKILL.md:487
- The recipe is not key-free as written: every contract invocation uses the undeclared local identity
alice, so it fails on a fresh installation unless that identity has first been configured. Use an existing public G-address such as$ISSUERfor--source-accountthroughout so the read-only checks require no local identity or secret.
Before you list an asset, display it, or accept it as collateral, answer seven
questions from the ledger itself. Everything below **simulates only** —
`--send=no` never signs or submits, and no step needs a key.
skills/cross-chain/layerzero.md:196
- This still reports only the pre-redeployment testnet failure. The PR verification plan requires re-testing the current EID 40600 endpoint and, if it still fails, documenting the new attempt; run that check and update this status with the current result before merge.
- **A testnet rehearsal is not guaranteed.** You can deploy your own OApp or OFT against the testnet endpoint (EID `40600`), but testnet sends failed with `#1213 UnsupportedMessageLib` in August 2026 — the required DVN did not support the endpoint's only registered message library. That endpoint has been redeployed since, and this file does not record a successful testnet send after it. Send one small testnet message and confirm delivery before you treat testnet as a rehearsal path.
skills/cross-chain/layerzero.md:206
- The PR's verification plan requires this OApp skeleton to compile for
wasm32v1-none, but this paragraph explicitly says no build was run. Signature comparison cannot validate macro expansion or exported methods, so the requested compile check must be completed and the verified revision/build result recorded before merge.
It is a skeleton, not a compiled artifact. Signature comparison does not catch macro expansion, feature, dependency, or target errors, and no build was run against this snippet. Compile it yourself for `wasm32v1-none` before you trust it, and confirm `lz_receive` appears in the exported interface.
skills/assets/SKILL.md:483
- This deep dive adds roughly 160 lines to
assets/SKILL.md, taking it past 700 lines. That conflicts with the repository rule to keepSKILL.mdbodies under ~500 lines and move deep dives into routed companion files (README.md:90,116); move the pre-listing recipe and result interpretation into an assets companion and leave a short router entry here.
### Pre-Listing Check (read-only)
skills/cross-chain/layerzero.md:145
min_amount_ldis a USDT0 token amount, not an XLM amount, so calling its units “stroops” is misleading. Describe it as 7-decimal USDT0 subunits; reserve stroops for the native XLM fee below.
read -r MIN_AMOUNT # the minimum the user accepts, in stroops
README asks for a SKILL.md under ~500 lines with deep dives in companion files. The pre-listing recipe pushed assets/SKILL.md to 709 lines, so it now lives in assets/pre-listing.md behind a short routed summary. Also from review: re-quote quote_oft with the accepted floor before pricing the message, name min_amount_ld's units (USDT0 7-decimal, not stroops), and check the recipient contract's TTL, because an instance archived before delivery takes the same G-address fallback.
|
🤖 Automated message from Kaan's Automated Triage Bot. Copilot found more real defects after my last update. All are fixed across
Sources: The evals moved with the teaching: cross-chain The build is green at One review point is now yours, @kaankacar. I reached my 10-round fix limit on this PR. Copilot's last point is fair. The recipe says no step needs a key, but every command uses Two checks are unchanged and still need you:
The merge is still yours. The file teaches transaction signing. I will keep tracking this PR. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (4)
Previously missed (1) — in code that hasn't changed since the last review.
skills/assets/pre-listing.md:37
aliceis a local identity that does not exist on a fresh machine, so this supposedly keyless recipe fails before simulation. Use the issuer's known publicG…address as the source account instead; no private key is needed with--send=no.
This issue also appears in the following locations of the same file:
- line 48
- line 63
- line 90
stellar contract invoke --id $SAC --source-account alice \
skills/assets/pre-listing.md:48
- These read-only OFT calls still depend on an undefined local
aliceidentity, contradicting the no-key recipe. Reuse the issuer public key as the simulation source.
stellar contract invoke --id $OFT --source-account alice \
skills/assets/pre-listing.md:90
- The final OneSig reads have the same hidden
aliceprerequisite. Point them at the existing issuer public key so a fresh machine can run the promised keyless workflow.
stellar contract invoke --id $ONESIG --source-account alice \
skills/assets/pre-listing.md:63
- The manager-role checks also fail unless the reader already configured an
aliceidentity. Use the public issuer address consistently so this section remains read-only and portable.
stellar contract invoke --id $MANAGER --source-account alice \
|
🤖 Automated message from Kaan's Automated Triage Bot. Copilot re-reviewed
Source: I withdraw the wording of my last comment on this one point. I repeated Copilot's reason without checking it. The suggestion is still a small improvement. The build is green at Two checks are unchanged and still need you, @kaankacar:
The merge is still yours. The file teaches transaction signing. I will keep tracking this PR. |
🤖 Automated message from Kaan's Automated Triage Bot.
Closes #115
This restores
layerzero.mdand anchors it on USDT0, the first production OFT on Stellar's endpoint.assetsgains the SAC-admin half: USDT0 as the live example, and a read-only pre-listing recipe.I re-ran the read-only checks on 2026-08-27 (mainnet, ledger 64151532):
GATISXX6…:auth_revocableandauth_clawback_enabledset, master key weight 0, nohome_domain.CBSJZEIO…matches Horizon's own derivation for the asset.SendParam,quote_oft,quote_send,send,OFTReceipt) matches LayerZero's Stellar crates and a live mainnetsend.ExecutorHelper.execute->lz_receive-> SAC manager (MINTER_ROLE) -> SAC mint.__lz_sendargument order matchmonorepo-externalat HEAD.Two corrections to the issue:
CBQOTWFU…; the metadata API now saysCALTBA5S…. Fixed.peer(eid)instead.Please do not merge this without two checks I could not run. Details in the issue thread, @kaankacar.