@@ -183,6 +183,28 @@ func NewTxBuilder(
183183// sign it, and the 64-byte signature (r||s) is passed to BroadcastOutboundSigningRequest.
184184// =============================================================================
185185
186+ // parseOutboundSender decodes data.Sender into a 20-byte push account. The
187+ // finalize/export paths derive the CEA PDA from it, so they require exactly 20
188+ // bytes. An automatic INBOUND_REVERT can instead carry a 32-byte Solana pubkey
189+ // (the original Solana sender), which the revert path never reads (it routes to
190+ // revert_recipient). In that case the 32-byte value is DISCARDED (not truncated)
191+ // and the zero account is returned — the refund destination comes from recipient.
192+ func parseOutboundSender (senderHex string , txType uetypes.TxType ) ([20 ]byte , error ) {
193+ var sender [20 ]byte
194+ senderBytes , err := hex .DecodeString (removeHexPrefix (senderHex ))
195+ if err != nil {
196+ return sender , fmt .Errorf ("invalid sender: %s" , senderHex )
197+ }
198+ if len (senderBytes ) == 20 {
199+ copy (sender [:], senderBytes )
200+ } else if txType != uetypes .TxType_INBOUND_REVERT {
201+ // Non-revert paths consume sender (CEA PDA seed) and must be exactly 20 bytes.
202+ return sender , fmt .Errorf ("invalid sender length: expected 20 bytes, got %d" , len (senderBytes ))
203+ }
204+ // INBOUND_REVERT with a non-20-byte sender: leave sender zero; it is unused.
205+ return sender , nil
206+ }
207+
186208// GetOutboundSigningRequest creates a signing request from an outbound event.
187209// Returns a 32-byte keccak256 hash that the TSS nodes need to sign.
188210func (tb * TxBuilder ) GetOutboundSigningRequest (
@@ -262,16 +284,9 @@ func (tb *TxBuilder) GetOutboundSigningRequest(
262284 copy (universalTxID [32 - len (utxIDBytes ):], utxIDBytes )
263285 }
264286
265- // sender: the 20-byte EVM address of the original sender on the source chain
266- var sender [20 ]byte
267- senderBytes , err := hex .DecodeString (removeHexPrefix (data .Sender ))
287+ sender , err := parseOutboundSender (data .Sender , txType )
268288 if err != nil {
269- return nil , fmt .Errorf ("invalid sender: %s" , data .Sender )
270- }
271- if len (senderBytes ) == 20 {
272- copy (sender [:], senderBytes )
273- } else {
274- return nil , fmt .Errorf ("invalid sender length: expected 20 bytes, got %d" , len (senderBytes ))
289+ return nil , err
275290 }
276291
277292 // PC20 export: core flags it via IsPc20; AssetAddr carries the 20-byte Push
@@ -800,15 +815,9 @@ func (tb *TxBuilder) BuildOutboundTransaction(
800815 copy (universalTxID [32 - len (utxIDBytes ):], utxIDBytes )
801816 }
802817
803- var sender [20 ]byte
804- senderBytes , err := hex .DecodeString (removeHexPrefix (data .Sender ))
818+ sender , err := parseOutboundSender (data .Sender , txType )
805819 if err != nil {
806- return nil , 0 , fmt .Errorf ("invalid sender: %s" , data .Sender )
807- }
808- if len (senderBytes ) == 20 {
809- copy (sender [:], senderBytes )
810- } else {
811- return nil , 0 , fmt .Errorf ("invalid sender length: expected 20 bytes, got %d" , len (senderBytes ))
820+ return nil , 0 , err
812821 }
813822
814823 // PC20 export: mirror of the signing-path branch — must rebuild the exact signed inputs.
0 commit comments