Summary
BytesEncoder::serialize in the TON SDK has a cell-tower flattening bug that loses the head cell and duplicates cell[1] when the tower contains 3 or more cells. Any LZ OApp on TON emitting payloads of 3+ cells (~224+ bytes) sends a corrupt lzMessage.
Affected File
packages/layerzero-v2/ton/contracts/protocol/msglibs/BytesEncoder.fc — function serialize.
Description
The loop uses secondLast = self.at(idx) which re-reads the just-processed index, so iteration never reaches at(0). The head cell (first 127 bytes, containing the ABI msgType/offset/length prefix) is lost and cell[1] is duplicated in its place.
Bug only manifests for towers of 3+ cells. Two-cell payloads (≤224 bytes) are unaffected, which makes the bug invisible for small messages and explains why it has gone undetected.
Reproduction
Sent a 256-byte ABI-encoded payload (8 slots, 3 cells) from a TON OApp:
- Expected outbound
lzMessage first byte: 0x01 (msgType)
- Actual:
0x030112ae… — the 0x03 is the last byte of nonce=3 positioned at offset 127, confirming the head cell is missing from the wire format.
Full repro with sandbox test:
https://github.com/cardanobuybot/tonstable/blob/fix/bytesencoder-3cell-payload-corruption/tests/OAppEncoder.spec.ts
Suggested Fix
Replace the broken loop with a single while (idx > 0) walk that folds at(idx) down to at(0) inclusive. Implemented locally via vendored copy:
https://github.com/cardanobuybot/tonstable/blob/fix/bytesencoder-3cell-payload-corruption/contracts/oapp/lz/protocol/msglibs/BytesEncoder.fc
After fix, both 2-cell and 3-cell payloads serialize correctly (verified via sandbox tests).
Impact
Any TON OApp emitting payloads ≥3 cells produces corrupt messages. On mainnet (active workers), this would cause downstream verification or decode to fail for every such message. On testnet the bug is masked when the path's worker rejects the message before decode.
Environment
- LZ V2 TON SDK (
packages/layerzero-v2/ton)
- TON Testnet (EID 40343) → Arbitrum Sepolia (EID 40231)
Happy to submit a PR with the fix if it would be helpful — just let me know preferred location and style.
Summary
BytesEncoder::serializein the TON SDK has a cell-tower flattening bug that loses the head cell and duplicatescell[1]when the tower contains 3 or more cells. Any LZ OApp on TON emitting payloads of 3+ cells (~224+ bytes) sends a corruptlzMessage.Affected File
packages/layerzero-v2/ton/contracts/protocol/msglibs/BytesEncoder.fc— functionserialize.Description
The loop uses
secondLast = self.at(idx)which re-reads the just-processed index, so iteration never reachesat(0). The head cell (first 127 bytes, containing the ABI msgType/offset/length prefix) is lost andcell[1]is duplicated in its place.Bug only manifests for towers of 3+ cells. Two-cell payloads (≤224 bytes) are unaffected, which makes the bug invisible for small messages and explains why it has gone undetected.
Reproduction
Sent a 256-byte ABI-encoded payload (8 slots, 3 cells) from a TON OApp:
lzMessagefirst byte:0x01(msgType)0x030112ae…— the0x03is the last byte ofnonce=3positioned at offset 127, confirming the head cell is missing from the wire format.Full repro with sandbox test:
https://github.com/cardanobuybot/tonstable/blob/fix/bytesencoder-3cell-payload-corruption/tests/OAppEncoder.spec.ts
Suggested Fix
Replace the broken loop with a single
while (idx > 0)walk that foldsat(idx)down toat(0)inclusive. Implemented locally via vendored copy:https://github.com/cardanobuybot/tonstable/blob/fix/bytesencoder-3cell-payload-corruption/contracts/oapp/lz/protocol/msglibs/BytesEncoder.fc
After fix, both 2-cell and 3-cell payloads serialize correctly (verified via sandbox tests).
Impact
Any TON OApp emitting payloads ≥3 cells produces corrupt messages. On mainnet (active workers), this would cause downstream verification or decode to fail for every such message. On testnet the bug is masked when the path's worker rejects the message before decode.
Environment
packages/layerzero-v2/ton)Happy to submit a PR with the fix if it would be helpful — just let me know preferred location and style.