Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions x/uexecutor/types/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,28 @@ func (p *Inbound) NormalizeForTxType() error {

// Route by the leading magic selector before decoding: a PC20 return (burn
// wrapper on the source chain -> unlock the locked native on Push) is flagged
// here and its selector stripped, so only the user payload is decoded and
// executed. An unrecognised/absent selector takes the PRC20 path unchanged.
// here via IsPc20 and its selector stripped for decoding. The full observed
// raw_payload (selector included) is intentionally left in place so that a
// failed decode preserves the exact on-chain bytes for forensics/debugging;
// raw_payload is cleared only after a successful decode. An unrecognised/absent
// selector takes the PRC20 path unchanged.
if p.RawPayload != "" {
isPC20, userPayload := RouteInboundPayload(p.RawPayload)
p.IsPc20 = isPC20
p.RawPayload = userPayload
}

// Decode raw_payload → universal_payload
if p.RawPayload != "" {
decoded, err := DecodeRawPayload(p.RawPayload, p.SourceChain)
if err != nil {
return fmt.Errorf("failed to decode raw payload: %w", err)
}
if decoded == nil {
return fmt.Errorf("raw_payload decoded to nil for payload tx type")
// Decode only the selector-stripped user payload → universal_payload.
if userPayload != "" {
decoded, err := DecodeRawPayload(userPayload, p.SourceChain)
if err != nil {
// Leave the full original raw_payload in place for forensics.
return fmt.Errorf("failed to decode raw payload: %w", err)
}
if decoded == nil {
return fmt.Errorf("raw_payload decoded to nil for payload tx type")
}
p.UniversalPayload = decoded
p.RawPayload = "" // clear after successful decode to save storage
}
p.UniversalPayload = decoded
p.RawPayload = "" // clear after successful decode to save storage
}
default:
// Non-payload types: payload is not used
Expand Down
76 changes: 76 additions & 0 deletions x/uexecutor/types/pc20_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,79 @@ func TestNormalizeForTxType_PC20(t *testing.T) {
require.Equal(t, to.Hex(), in.UniversalPayload.To)
})
}

// NormalizeForTxType must preserve the FULL observed raw_payload (magic selector
// included) when the user payload fails to decode, so the exact on-chain bytes
// remain available for forensics/debugging. Previously the selector was stripped
// into raw_payload before decoding, so a failed decode left only the stripped
// remainder — losing the selector that determined is_pc20. A successful decode
// still clears raw_payload.
func TestNormalizeForTxType_PreservesRawPayloadOnDecodeFailure(t *testing.T) {
const evmChain = "eip155:11155111"

// A valid, decodable EVM universal payload (for the success case).
to := common.HexToAddress("0x000000000000000000000000000000000000beef")
validEncoded, err := abiEncodeUniversalPayload(
to, big.NewInt(1000), []byte{0xde, 0xad, 0xbe, 0xef},
big.NewInt(21000), big.NewInt(1000000000), big.NewInt(200000000),
big.NewInt(1), big.NewInt(9999999999), 1,
)
require.NoError(t, err)

// A body that is NOT a valid ABI-encoded UniversalPayload → decode fails.
const badBody = "deadbeef"

t.Run("PC20 decode failure keeps the full original raw_payload", func(t *testing.T) {
original := "0x" + types.PC20Selector + badBody
in := types.Inbound{
SourceChain: evmChain,
TxType: types.TxType_FUNDS_AND_PAYLOAD,
RawPayload: original,
}
err := in.NormalizeForTxType()
require.Error(t, err, "an undecodable body must fail normalization")
require.True(t, in.IsPc20, "PC20 selector must still be flagged on failure")
require.Nil(t, in.UniversalPayload, "no universal payload on failure")
require.Equal(t, original, in.RawPayload,
"raw_payload must retain the FULL original (selector included) on decode failure")
})

t.Run("PRC20 decode failure keeps the full original raw_payload", func(t *testing.T) {
original := "0x" + types.PRC20Selector + badBody
in := types.Inbound{
SourceChain: evmChain,
TxType: types.TxType_FUNDS_AND_PAYLOAD,
RawPayload: original,
}
err := in.NormalizeForTxType()
require.Error(t, err)
require.False(t, in.IsPc20, "PRC20 selector is not PC20")
require.Equal(t, original, in.RawPayload,
"raw_payload must retain the full original (PRC20 selector included) on failure")
})

t.Run("un-prefixed decode failure keeps the original raw_payload", func(t *testing.T) {
original := "0x" + badBody
in := types.Inbound{
SourceChain: evmChain,
TxType: types.TxType_FUNDS_AND_PAYLOAD,
RawPayload: original,
}
err := in.NormalizeForTxType()
require.Error(t, err)
require.False(t, in.IsPc20)
require.Equal(t, original, in.RawPayload)
})

t.Run("PC20 successful decode clears raw_payload", func(t *testing.T) {
in := types.Inbound{
SourceChain: evmChain,
TxType: types.TxType_FUNDS_AND_PAYLOAD,
RawPayload: "0x" + types.PC20Selector + strings.TrimPrefix(validEncoded, "0x"),
}
require.NoError(t, in.NormalizeForTxType())
require.True(t, in.IsPc20)
require.NotNil(t, in.UniversalPayload)
require.Empty(t, in.RawPayload, "raw_payload must be cleared after a successful decode")
})
}
Loading