specs(exact): propose TON exact scheme for x402 v2 (spec-only)#1455
specs(exact): propose TON exact scheme for x402 v2 (spec-only)#1455ohld wants to merge 30 commits intox402-foundation:mainfrom
Conversation
🟡 Heimdall Review Status
|
|
@ohld is attempting to deploy a commit to the Coinbase Team on Vercel. A member of the Team first needs to authorize it. |
fbf520a to
86c1427
Compare
|
Really excited to see TON getting a proper x402 spec — the W5/ 1. 2. 3. Relay commission amount is undefined |
|
Thanks for the thorough review, these are all great catches. 1. You're right — 2. Good call. I didn't want to hardcode a specific hash in the spec since W5 has revisions (v5r1, potentially v5r2) and a hardcoded hash would become outdated when TON Foundation ships a new version. Instead I added: facilitator MUST verify that the contract code in 3. Relay commission bounds Added Also tightened rule §4: the W5 message must contain exactly two actions (payment transfer + optional commission transfer), anything else is rejected. All changes in the latest commit + updated the demo repo to enforce exact amount matching (30/30 tests passing). |
Adds scheme_exact_ton.md defining the exact payment scheme for TON blockchain. Uses W5 wallet internal_signed messages for gasless Jetton (TEP-74) transfers. Updates scheme_exact.md with TON-specific validation rules.
… bounds, stateInit verification - Change amount verification from >= to == (exact match), aligning with all other chain specs (SVM, Stellar, EVM). The relay commission is a separate jetton_transfer in the W5 batch, not bundled into the payment. - Add extra.maxRelayCommission field to PaymentRequirements, allowing resource servers to cap the relay commission amount. - Add stateInit verification rule: when seqno == 0, facilitator must verify contract code against a known W5 wallet code hash allowlist. - Tighten action count: W5 message must contain exactly the payment transfer and optionally a relay commission transfer, nothing else.
a418dd0 to
9f6683f
Compare
|
Thanks a lot for putting this together @ohld! Notified the core team to review. Please be aware that we currently have a lot of pending/incoming requests, so it might take a bit until we can get back to you, thanks for your patience 🙏 |
|
While we wait for the spec review, I created a facilitator for TON https://github.com/ohld/x402-ton-facilitator And our community also implemented one: https://github.com/TONresistor/x402-ton We'll start implementing all SDKs as well. |
Add @x402/tvm (TypeScript) and x402[tvm] (Python) mechanism packages implementing the exact payment scheme for TON blockchain. Python (mechanisms/tvm/): - Full gasless USDT payment flow via TONAPI relay - Ed25519 signature verification for W5R1 wallets - BoC parser for external messages, jetton transfers - 6-rule payment verification (protocol, signature, intent, replay, relay safety, simulation) - Idempotent settlement with state machine - 72 unit tests TypeScript (@x402/tvm): - SchemeNetworkClient/Server/Facilitator implementations - W5R1 wallet signing with @ton/ton SDK - Gasless estimate + settlement via TONAPI - CAIP-2 network IDs: tvm:-239 (mainnet), tvm:-3 (testnet) - 48 unit tests Refs: spec PR coinbase#1455, live facilitator at ton-facilitator.okhlopkov.com
|
We implemented this spec in production with x402-ton (https://github.com/TONresistor/x402-ton). Two findings from our integration: Commission discovery at boot: The spec recommends discovering commission via Native TON support: Our implementation supports both USDT gasless (internal auth via TONAPI) and native TON direct broadcast (external auth). The PR describes the jetton path only. We treat native TON as an additive superset since payers with TON already have gas and don't need the gasless relay. Live facilitator: https://x402.resistance.dog |
|
|
||
| - `payload.seqno` MUST match the wallet's current on-chain seqno. | ||
| - Duplicate `signedBoc` submissions MUST be rejected. | ||
| - The W5 message MUST contain exactly two actions: the payment `jetton_transfer` and (optionally) a relay commission `jetton_transfer`. Any additional actions MUST cause rejection. |
There was a problem hiding this comment.
| - The W5 message MUST contain exactly two actions: the payment `jetton_transfer` and (optionally) a relay commission `jetton_transfer`. Any additional actions MUST cause rejection. | |
| - The W5 message MUST contain at most two actions: the payment `jetton_transfer` and (optionally) a relay commission `jetton_transfer`. Any additional actions MUST cause rejection. |
| - `payTo`: Recipient TON address (raw format). | ||
| - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). | ||
| - `extra.relayAddress`: (Optional) Gasless relay address that receives the relay commission. When present, the W5 batch includes a separate `jetton_transfer` to this address as commission for gas sponsorship. When absent, the client handles gas fees directly. | ||
| - `extra.maxRelayCommission`: (Optional) Maximum relay commission in atomic token units. When present, facilitator MUST reject W5 batches where the commission transfer exceeds this amount. When absent, the facilitator applies its own commission policy. |
There was a problem hiding this comment.
Who effectively pays the relayFee in the end? Is the client sending amount + relayFee or is the server receiving amount - relayFee?
| - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). | ||
| - `payTo`: Recipient TON address (raw format). | ||
| - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). | ||
| - `extra.relayAddress`: (Optional) Gasless relay address that receives the relay commission. When present, the W5 batch includes a separate `jetton_transfer` to this address as commission for gas sponsorship. When absent, the client handles gas fees directly. |
There was a problem hiding this comment.
Could you please clarify if the facilitator acts as relayer or is the relayer an independent 3rd party actor?
There was a problem hiding this comment.
If its a a 3rd party, how does the server know to which relay service the facilitator will submit the tx? Ie how to get relayAddress?
|
|
||
| The `exact` scheme on TON transfers a specific amount of a [TEP-74] Jetton from the client to the resource server using a W5 wallet `internal_signed` message. | ||
|
|
||
| The facilitator sponsors gas by wrapping the client-signed message in an internal TON message. The client controls payment intent (asset, recipient, amount) through Ed25519 signature. The facilitator cannot modify the destination or amount. |
There was a problem hiding this comment.
From reading through the specs, it is my understanding that the client would sign two batched transfers to payTo and relayAddress. The facilitator wouldn't actually sponsor gas? The facilitator would effectively just pass through the client request to the relayer?
If this is the case, I think phrasing the relayer service as gas sponsorship is also a bit misleading. Its more like a mechanism for the client to pay for gas in a non-native token. But the client still effectively pays for the gas
|
|
||
| 1. Re-run all verification checks (do not trust prior `/verify` result). | ||
| 2. Submit `signedBoc` via gasless relay or direct broadcast: | ||
| - **Sponsored (gasless):** `POST /v2/gasless/send` with `{ wallet_public_key, boc }` to a relay service. The relay wraps the signed message in an internal message carrying TON for gas. |
There was a problem hiding this comment.
This sounds like relayer is an independent service
- Check payload.to == requirements.payTo (metadata consistency) - Verify source Jetton wallet via get_wallet_address(from) on master - Fix dead code: old check used transfer.jetton_wallet which was always empty - Add negative tests for both new checks Per @skywardboundd review on coinbase/x402#1455
Per @skywardboundd review: 1. payload.to MUST equal requirements.payTo (explicit metadata check) 2. Source Jetton wallet (W5 internal msg destination) MUST match get_wallet_address(from) — prevents substitute source contract 3. jetton_transfer body destination MUST equal requirements.payTo Tested against deployed facilitator with real on-chain data. Facilitator commit: ohld/x402-ton-facilitator@6f92320
|
Good catches, both added in the latest commit:
Also added a third explicit rule: All three checks are implemented and tested in the facilitator against mainnet via TONAPI. |
| - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). | ||
| - `payTo`: Recipient TON address (raw format). | ||
| - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). | ||
| - `extra.facilitatorUrl`: URL of the facilitator server. The resource server calls `{facilitatorUrl}/verify` and `{facilitatorUrl}/settle`. |
There was a problem hiding this comment.
facilitatorUrl is not needed, please remove
| 1. After verification succeeds, compute a hash of the `settlementBoc`. | ||
| 2. If the hash is already present in the cache, reject the settlement with a `"duplicate_settlement"` error. | ||
| 3. If the hash is not present, insert it into the cache and proceed with signing and submission. | ||
| 4. Evict entries older than `maxTimeoutSeconds` from the corresponding `PaymentRequirements`. After this window, the signed message will have expired and cannot land on-chain regardless. |
There was a problem hiding this comment.
maxTimeoutSeconds is a server-side setting that covers time for API execution + tx confirmation.
SVM caches around /settle for a fixed 2mins after which the blockhash will be expired.
Could we also defined a fixed time here after which the seqno should have safely advanced onchain?
| - `payload.settlementBoc` MUST decode as a valid TON external message. | ||
| - The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). | ||
| - The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). | ||
| - If the external message includes `stateInit` (seqno == 0), the facilitator MUST verify the contract code matches a known W5 wallet contract. Implementations SHOULD maintain an allowlist of accepted wallet code hashes. |
There was a problem hiding this comment.
What would be the size of the allowlist currently and how frequently do you anticipate this to change?
We should avoid scenarios where some clients can only use some facilitators based on an opaque allowlist the facilitator keeps internally. Might be worth to define a canonical set of code hashes that MUST be accepted.
For SVM, we want to move to simulation based verification instead of an explicit smart wallet whitelist, see
https://github.com/coinbase/x402/pull/1527. Could a similar approach work here?
I think for now we could live with an allowlist, but something to consider for future updates if feasible
There was a problem hiding this comment.
The allowlist currently has 1 entry: W5R1. I added the canonical code hash inline in the latest commit so implementations don't need to discover it externally.
TON wallet versions ship very rarely. v3 to v4 and v4 to v5 were years apart each, so in practice this list is near-static. When a new version does ship, it would be announced by TON Foundation and implementations would add the new hash.
Regarding simulation-based verification: yes, this works on TON. The emulation API runs the full transaction trace, so if the contract doesn't produce the expected jetton transfer, the output won't match and verification fails. I've noted this in the spec: implementations that simulate MAY skip explicit code hash checks. If SVM moves to simulation-only (PR #1527), we can follow the same direction here.
| - `payload.settlementBoc` MUST decode as a valid TON external message. | ||
| - The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). | ||
| - The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). | ||
| - If the external message includes `stateInit` (seqno == 0), the facilitator MUST verify the contract code matches a known W5 wallet contract. Implementations SHOULD maintain an allowlist of accepted wallet code hashes. |
There was a problem hiding this comment.
The wallet contract check is only done for seqno == 0. Would it be possible to deploy a malicious wallet contract that could grief the facilitator for seqno>0 by eg keeping the TON but not executing the Jetton transfer? I suppose the simulation checks would fail then, just to double-check with you that there is no attack vector
There was a problem hiding this comment.
Good question. Short answer: simulation catches this, and without simulation the risk is limited to gas (~$0.04).
The key property of W5R1 is that it has no setCode opcode. Once deployed, the contract code is immutable. So a wallet that was deployed as W5R1 at seqno=0 cannot become malicious later. cc @skywardboundd
The remaining scenario is a non-W5R1 contract that was never deployed via x402 (so no stateInit check happened). If such a contract accepts the facilitator's TON but doesn't execute the jetton transfer, simulation will show that the expected transfer output is missing and verification will fail before settlement.
Without simulation, the facilitator would lose gas (~0.013 TON / $0.04 per attempt), but the payment itself won't execute, so no funds are stolen. Economically irrational for the attacker since they gain nothing.
For extra safety, implementations can also query the on-chain code hash for any wallet address (not just seqno=0). But simulation is the cleaner path forward.
… W5R1 hash - Remove facilitatorUrl from extra (implementation detail, not protocol field) - Fix dedup TTL: use fixed 300s instead of per-request maxTimeoutSeconds - Add canonical W5R1 code hash for stateInit verification - Note simulation as alternative to code hash checks for seqno>0 Per @phdargen review on coinbase/x402#1455
| 4. **Client** constructs a `jetton_transfer` body ([TEP-74]) and wraps it in a W5 `internal_signed` message. | ||
| 5. **Client** signs the message with their Ed25519 private key. | ||
| 6. **Client** wraps the signed body in an external message BOC (with `stateInit` if `seqno == 0`) and base64-encodes it. |
There was a problem hiding this comment.
It’s not entirely clear why we wrap internal_body and state_init in an external message. I think it would be better to split settlementBoc into separate fields, such as internalBoc and stateInitBoc, since that would be easier to understand. If we want to keep a single field, then it would make more sense to encode it as an internal message rather than an external one, because the internal message is what the facilitator will actually send to the network.
There was a problem hiding this comment.
Agree, I think Internal Message will work in this case. In includes all the useful data required for the future transaction processing
There was a problem hiding this comment.
Good point. We considered splitting into separate internalBoc + stateInitBoc fields, but went with encoding as a single internal message since it naturally carries both the body and stateInit in one structure. Updated in the latest commit: settlementBoc is now an internal message (bounceable, dest=client wallet, value=0). The facilitator extracts body + stateInit from it.
| - `tokenMaster`: Jetton master contract address in raw format. Must match `requirements.asset`. | ||
| - `amount`: Payment amount in atomic token units. Must match `requirements.amount`. | ||
| - `validUntil`: Unix timestamp after which the signed message expires. | ||
| - `settlementBoc`: Base64-encoded signed W5 external message BOC containing the Jetton transfer with `internal_signed` body and Ed25519 signature. |
There was a problem hiding this comment.
In exact_scheme.md and in the facilitator reference implementation, support for external_signed messages is mentioned, whereas exact_scheme_ton.md only refers to internal_signed.
There was a problem hiding this comment.
Right, cleaned this up. The current spec now explicitly scopes to internal_signed only (gasless flow). Added a note at the top that non-gasless flows (external_signed, native TON) are planned for a follow-up extension.
|
|
||
| - `payload.settlementBoc` MUST decode as a valid TON external message. | ||
| - The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). | ||
| - The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). |
There was a problem hiding this comment.
There should be a check that payload.walletPublicKey matches the public key actually obtained from the blockchain for the address payload.from, or from the wallet’s stateInit. With that approach, passing payload.walletPublicKey separately is generally unnecessary.
There was a problem hiding this comment.
Agreed, removed walletPublicKey from the payload entirely. The facilitator now derives the public key from the stateInit data cell (seqno == 0) or via the on-chain get_public_key getter (seqno > 0). Much cleaner.
| "from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", | ||
| "to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", | ||
| "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", | ||
| "amount": "10000", | ||
| "validUntil": 1772689900, |
There was a problem hiding this comment.
All of these fields can be derived from settlementBoc. If included explicitly, they must be additionally validated, which is a poor developer experience.
There was a problem hiding this comment.
Except for the tokenMaster. This field maybe include for the future protocol extension with multiple accepted assets. But let's rename it to "assert" for clarity
There was a problem hiding this comment.
Done, renamed to asset in the payload. Matches requirements.asset and leaves room for multi-asset extensions.
| 4. **Client** constructs a `jetton_transfer` body ([TEP-74]) and wraps it in a W5 `internal_signed` message. | ||
| 5. **Client** signs the message with their Ed25519 private key. | ||
| 6. **Client** wraps the signed body in an external message BOC (with `stateInit` if `seqno == 0`) and base64-encodes it. |
There was a problem hiding this comment.
Agree, I think Internal Message will work in this case. In includes all the useful data required for the future transaction processing
|
|
||
| **Field Definitions:** | ||
|
|
||
| - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). |
There was a problem hiding this comment.
Are there future plans for non-gasless TEP-74 transfers and native TON transfers? The spec is built only for gasless payments, with the required facilitator entity. In the future it'll be hard to extend this document with another payments. Maybe it is better to plan them ahead?
There was a problem hiding this comment.
Yes, we want to support non-gasless TEP-74 transfers and native TON transfers in follow-up specs. Added a note in the scope section. The current spec is intentionally focused on the gasless flow to keep the first version simple, but the internal message format we chose should make it straightforward to extend.
| ### 3. Facilitator safety | ||
|
|
||
| - The facilitator's own address MUST NOT appear as the sender (`payload.from`) or as the source of any Jetton transfer. This prevents a malicious payload from tricking the facilitator into spending its own funds. |
There was a problem hiding this comment.
I think that this makes no sense. How the payload would trick the facilitator if he will emulate before sending funds?
There was a problem hiding this comment.
Fair point. Updated the spec: implementations that simulate MAY skip explicit code hash checks (simulation catches any malicious contract behavior). The code hash check remains as a requirement only for implementations that don't simulate.
| 1. **Client** requests a protected resource from the **Resource Server**. | ||
| 2. **Resource Server** responds with HTTP 402 and `PaymentRequired` data. The `accepts` array includes a TON payment option. | ||
| 3. **Client** queries a TON RPC endpoint to resolve its Jetton wallet address (`get_wallet_address` on the Jetton master contract) and fetches its current wallet seqno. | ||
| 4. **Client** constructs a `jetton_transfer` body ([TEP-74]) and wraps it in a W5 `internal_signed` message. |
There was a problem hiding this comment.
Fixed, the internal message in settlementBoc MUST be bounceable now.
| ### 5. Replay protection | ||
|
|
||
| - `payload.validUntil` MUST NOT be expired and MUST NOT be more than `maxTimeoutSeconds` in the future. | ||
| - The wallet's on-chain seqno MUST be checked: the seqno in the BoC MUST NOT be less than the current on-chain seqno. |
There was a problem hiding this comment.
On TON seqno should be strictly equal to stored. Is there any business requirements behind this (delayed send)? Or just a missprint?
There was a problem hiding this comment.
Misprint on our side, thanks. Fixed to strict equality. TON wallets reject anything that doesn't match exactly.
|
|
||
| ## Settlement Logic | ||
|
|
||
| 1. Re-run all verification checks (do not trust prior `/verify` result). |
There was a problem hiding this comment.
By this is it meant that Resource Server should do the emulation as well? In this case why /verify is needed?
There was a problem hiding this comment.
The /verify step is an x402 protocol-level requirement, not TON-specific. It lets the resource server reject invalid payloads before doing any actual work (API execution, data retrieval, etc). The facilitator then re-verifies independently during /settle.
@phdargen would be great to hear your thoughts on this too. Is there a better way to frame the verify/settle separation in the spec for chains where simulation covers everything?
There was a problem hiding this comment.
Facilitators should reverify cheap checks like payload fields in settle/, the may skip checks that require rpc calls like balance checks or simulation if they want to optimize for latency, at the risk of wasting gas for edge cases where tx become invalid between verify/ and settle/.
For EVM, we have a config flag to give facilitators the choice:
|
Review Error for Alejandbel @ 2026-03-24 15:04:22 UTC |
Major rework based on TON Core team review:
- settlementBoc is now an internal message (not external)
- Payload reduced to {settlementBoc, asset}, all fields derived from BoC
- Public key derived from stateInit or on-chain get_public_key
- Seqno check changed to strict equality (TON requirement)
- Internal message must be bounceable
- Removed facilitatorUrl, nonce, walletPublicKey
- Clarified simulation in /verify, extensibility for non-gasless flows
- Updated parent scheme_exact.md TON section
|
|
||
| The facilitator IS the relay. It sponsors gas (~0.013 TON per transaction) by wrapping the client-signed message in an internal TON message from its own funded wallet. The client resolves signing data (seqno, Jetton wallet address) via a TON RPC endpoint, signs locally, and sends the result. The facilitator cannot modify the destination or amount; the client controls payment intent through Ed25519 signature. | ||
|
|
||
| There is no relay commission. The facilitator absorbs gas costs as the cost of operating the payment network, analogous to how EVM facilitators pay gas for `transferWithAuthorization`. |
There was a problem hiding this comment.
The facilitator operator funds their own wallet, same model as EVM where the facilitator pays gas for transferWithAuthorization.
| 7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload`. | ||
| 8. **Resource Server** forwards the payload and requirements to the **Facilitator's** `/verify` endpoint. | ||
| 9. **Facilitator** deserializes the internal message BoC, derives the sender address and public key, verifies the Ed25519 signature, validates payment intent (amount, destination, asset), and checks replay protection (seqno, validUntil, BoC hash). | ||
| 10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it protects the resource server from doing unnecessary work for invalid payloads. This is an x402 protocol-level requirement, not specific to TON. |
There was a problem hiding this comment.
This is an x402 protocol-level requirement, not specific to TON.
Do we really need this sentence?
|
|
||
| - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). | ||
| - `payTo`: Recipient TON address (raw format). | ||
| - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). |
There was a problem hiding this comment.
There is no comments how we can get decimal parameter
There was a problem hiding this comment.
Added a note that decimals can be queried via get_jetton_data on the Jetton master contract.
| The facilitator derives the following from the BoC: | ||
| - **Sender address**: the `dest` field of the internal message (the client's wallet). | ||
| - **Public key**: from the `stateInit` data cell (if present) or via the on-chain `get_public_key` getter. | ||
| - **Amount, destination, validUntil, seqno**: from the W5 signed body and its actions. |
| - `payload.accepted.payTo` MUST equal `requirements.payTo`. | ||
| - `payload.accepted.amount` MUST equal `requirements.amount` exactly. | ||
|
|
||
| ### 2. Signature validity |
There was a problem hiding this comment.
It isn't about client signature validity
There was a problem hiding this comment.
Renamed to "Message and signature verification".
|
|
||
| - Facilitator SHOULD simulate message execution via emulation during `/verify`. This protects the resource server from doing unnecessary work for invalid payloads. | ||
| - Verification SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, or invalid seqno. | ||
| - When simulation is performed, it implicitly covers seqno, balance, and code hash checks from sections 2 and 5. |
There was a problem hiding this comment.
about what code hash check we are talking?
There was a problem hiding this comment.
The W5 wallet code hash check in section 2 (hash 20834b7b...). When you simulate, this is covered implicitly since a non-W5 contract would not execute the expected transfer.
There was a problem hiding this comment.
The W5 wallet code hash check in section 2 (hash
20834b7b...). When you simulate, this is covered implicitly since a non-W5 contract would not execute the expected transfer.
No, it does not work like this. You can make fake contract, that won't fail the emulation, but the transaction will not be properly executed.
| 2. Extract the signed body and optional `stateInit` from the internal message BoC. | ||
| 3. Fetch the facilitator's own wallet seqno. | ||
| 4. Estimate gas via emulation: build a trial relay message, emulate the trace, and sum all fees across the trace. | ||
| 5. Build the relay message: wrap the user's signed body in a bounceable internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. If the user's wallet has a `stateInit` (seqno == 0), include it in the relay message for deployment. |
There was a problem hiding this comment.
If the user's wallet has a
stateInit(seqno == 0), include it in the relay message for deployment.
We should add stateinit if account is not active
https://docs.ton.org/foundations/status
There was a problem hiding this comment.
Fixed. Changed to reference account state (nonexist/uninit) instead of seqno == 0.
There was a problem hiding this comment.
What we should do on frozen accounts?
| 4. Estimate gas via emulation: build a trial relay message, emulate the trace, and sum all fees across the trace. | ||
| 5. Build the relay message: wrap the user's signed body in a bounceable internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. If the user's wallet has a `stateInit` (seqno == 0), include it in the relay message for deployment. | ||
| 6. Sign and broadcast the facilitator's external message. | ||
| 7. Wait for transaction confirmation (typically < 5 seconds on TON). |
|
|
||
| A race condition exists in the settlement flow: if the same payment BoC is submitted to the facilitator's `/settle` endpoint multiple times before the first submission is confirmed on-chain, each call may attempt broadcast. Although TON's seqno-based replay protection ensures the transfer only executes once on-chain, a malicious client can exploit the timing window to obtain access to multiple resources while only paying once. | ||
|
|
||
| ### Recommended Mitigation |
There was a problem hiding this comment.
Why are we talking about this? If we want to protect ourselves from the vulnerability mentioned above, then either simulation or checking the seqno helps.
There was a problem hiding this comment.
This covers a specific race condition: if the same BoC is submitted twice before the first settlement lands on-chain, both /verify calls pass because the seqno hasn't changed yet. The attacker gets 2 resource accesses for 1 payment. Seqno/simulation don't help here because on-chain state is unchanged between the two calls. The cache closes this window. It's RECOMMENDED, not MUST.
| |---|---|---|---|---| | ||
| | `tvm:-239` | USDT | USD₮ | 6 | `0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe` | | ||
|
|
||
| ### References |
There was a problem hiding this comment.
|
Review Error for skywardboundd @ 2026-03-25 12:25:40 UTC |
…tion, simulation chain - Replace seqno==0 with proper TON account states (nonexist/uninit) for stateInit condition - Clarify that payload.asset is a JSON field, not a BoC-internal field (TEP-74 has no asset) - Add full TEP-74 transfer chain verification to simulation section - Remove non-normative timing note from settlement step 7 - Add W5 gasless transactions reference link
- Narrow wallet scope to W5 v5r1 (not v5r1+), only version that exists today - Remove redundant "x402 protocol-level requirement" sentence - Add get_jetton_data hint for decimal resolution - Add walletId to derived fields list - Rename section 2 to "Message and signature verification" - Move balance check from Replay protection to Payment intent - Remove redundant seqno explanation sentence
|
|
||
| - Facilitator SHOULD simulate message execution via emulation during `/verify`. This protects the resource server from doing unnecessary work for invalid payloads. | ||
| - Verification SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, or invalid seqno. | ||
| - When simulation is performed, it implicitly covers seqno, balance, and code hash checks from sections 2 and 5. |
There was a problem hiding this comment.
The W5 wallet code hash check in section 2 (hash
20834b7b...). When you simulate, this is covered implicitly since a non-W5 contract would not execute the expected transfer.
No, it does not work like this. You can make fake contract, that won't fail the emulation, but the transaction will not be properly executed.
|
Review Error for ArkadiyStena @ 2026-03-26 20:08:11 UTC |
|
|
||
| ### 3. Facilitator safety | ||
|
|
||
| - The facilitator's own address MUST NOT appear as the sender (derived from the BoC's `dest` field) or as the source of any Jetton transfer. This prevents a malicious payload from tricking the facilitator into spending its own funds. |
There was a problem hiding this comment.
This should be removed as it doesn't make any sense. User can't send any transactions that will affect facilitator's wallet.
Refine the TON `exact` scheme spec to better describe how client-signed W5 jetton transfers are verified and relayed by the facilitator. This update expands the TON payment requirements and verification rules to account for optional jetton transfer parameters exposed through `extra`, and clarifies how those parameters are interpreted when they are omitted. Changes included: - add optional `extra.forwardPayload` and `extra.forwardTonAmount` to TON `PaymentRequirements` and define their effective defaults as zero-bit payload and `"0"` - add optional `extra.responseDestination` and define its effective default as `addr_none` - require facilitators to compare the effective values of `responseDestination`, `forwardPayload`, and `forwardTonAmount` between `accepted.extra` and `requirements.extra` - extend payment intent validation so the facilitator derives and checks `response_destination`, `forward_payload`, and `forward_ton_amount` from the signed W5 action - clarify that the facilitator-funded relay must cover both outer relay execution and the value expected by the client-signed inner transfer - clarify that the client-signed inner message must carry enough TON to fund payer-side execution from the payer jetton wallet to the source jetton wallet - specify that the outbound internal message from the client W5 wallet to the source jetton wallet must be bounceable - specify that the `settlementBoc` wrapper itself does not need to be bounceable - clean up terminology and consistency across the document: `BoC`, `zero-bit`, `forwardPayload`, `forwardTonAmount`, and derived field descriptions This is a spec clarification/update only. It does not introduce a new TON payment scheme, but makes the existing gas-sponsored W5 relay flow more precise.
|
There have been significant changes in the last commit by @ArkadiyStena, could you guys please give a TLDR of what changed and why? In particular, motivating the new extra fields. There are open questions in the thread from @skywardboundd about:
Does the last commit address those? Any other open issues/question from the thread above @ohld? Will make a first pass on your sdk PR next week, please tag me when it reflects the current state of the specs |
|
@phdargen Thanks. TL;DR: the last commit was mostly a spec clarification pass around TON-specific Jetton transfer semantics, not a redesign of the flow. Main changes:
Why the new
On the open questions:
|
Description
Adds formal specification for the
exactpayment scheme on TON blockchain, following the same structure as existing network-specific scheme documents (EVM, SVM, Stellar, Aptos).specs/schemes/exact/scheme_exact_ton.md-- full spec for TON exact schemespecs/schemes/exact/scheme_exact.md-- adds TON validation rules to the indexTests
No code affected.
Checklist
Why TON
Working proof
Key design decisions
v2 alignment
Review request
Please review the scheme design, payload shape, and verification/settlement safety constraints. Once aligned, I will open a separate implementation PR in TypeScript.