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
39 changes: 37 additions & 2 deletions universalClient/externalchains/svm/tx_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ var (
rateLimitConfigSeed = []byte("rate_limit_config")
tokenRateLimitSeed = []byte("rate_limit")
storedIxDataSeed = []byte("stored_ix_data")
eventAuthoritySeed = []byte("__event_authority")

// TSS message envelope — cross-protocol replay guard.
tssMessagePrefix = []byte("PUSH_CHAIN_SVM")
Expand Down Expand Up @@ -1468,6 +1469,16 @@ func (tb *TxBuilder) deriveTSSPDA() (solana.PublicKey, error) {
return address, err
}

// deriveEventAuthorityPDA derives the account Anchor's #[event_cpi] macro requires
// on every instruction that may self-CPI to emit an event. Every gateway
// instruction that emits an event needs this PDA, plus the gateway program
// itself, appended after its named accounts and before any remaining accounts.
func (tb *TxBuilder) deriveEventAuthorityPDA() (solana.PublicKey, error) {
seeds := [][]byte{eventAuthoritySeed}
address, _, err := solana.FindProgramAddress(seeds, tb.gatewayAddress)
return address, err
}

// fetchTSSChainID reads the TSS PDA account from on-chain and extracts the chain ID.
//
// On-chain layout (Borsh-serialized TssPda struct from state.rs):
Expand Down Expand Up @@ -2079,8 +2090,11 @@ func (tb *TxBuilder) buildRescueData(
// --- Optional ref-finalize accounts (19-20) ---
// 19 stored_ix_data read/None StoredIxData PDA (only used by ref-finalize route)
// 20 store_refund_recipient mut/None Receives store-tx fee reimbursement (ref route only)
// --- #[event_cpi] accounts (21-22) ---
// 21 event_authority read-only PDA ["__event_authority"], for the gateway's self-CPI
// 22 program read-only The gateway program itself
// --- Execute-only remaining accounts ---
// 21+ remaining_accounts varies Accounts that the target program needs
// 23+ remaining_accounts varies Accounts that the target program needs
//
// For Anchor Option<Account> fields: passing the gateway program's own ID = None.
// This is Anchor's convention for encoding "this optional account is not provided".
Expand Down Expand Up @@ -2203,6 +2217,14 @@ func (tb *TxBuilder) buildWithdrawAndExecuteAccounts(
accounts = append(accounts, &solana.AccountMeta{PublicKey: storeRefundRecipient, IsWritable: true, IsSigner: false})
}

// #[event_cpi] accounts (#21-22): required on every named-account boundary
// before remaining_accounts, so the gateway can self-CPI its emit_cpi event.
eventAuthority, _ := tb.deriveEventAuthorityPDA()
accounts = append(accounts,
&solana.AccountMeta{PublicKey: eventAuthority, IsWritable: false, IsSigner: false},
&solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false},
)

// For execute mode: append the target program's accounts as "remaining_accounts".
// These are the accounts that the gateway will pass through via CPI to the target program.
if instructionID == 2 {
Expand Down Expand Up @@ -2233,11 +2255,16 @@ func (tb *TxBuilder) buildWithdrawAndExecuteAccounts(
// 6 executed_sub_tx mut Replay protection (gets created)
// 7 caller signer,mut Relayer
// 8 system_program read-only
// --- Optional SPL accounts (9-12) ---
// --- Optional SPL accounts (9-14) ---
// 9 token_vault mut/None Vault's ATA for the token
// 10 recipient_token_account mut/None Recipient's ATA
// 11 token_mint read/None The SPL token mint
// 12 token_program read/None SPL Token program
// 13 associated_token_program read/None Needed to create the recipient ATA
// 14 rent read/None Needed to create the recipient ATA
// --- #[event_cpi] accounts (15-16) ---
// 15 event_authority read-only PDA ["__event_authority"], for the gateway's self-CPI
// 16 program read-only The gateway program itself
func (tb *TxBuilder) buildRevertAccounts(
configPDA solana.PublicKey,
vaultPDA solana.PublicKey,
Expand Down Expand Up @@ -2286,6 +2313,14 @@ func (tb *TxBuilder) buildRevertAccounts(
)
}

// #[event_cpi] accounts: required so the gateway can self-CPI its emit_cpi
// event. revert_universal_tx has no remaining_accounts, so these are last.
eventAuthority, _ := tb.deriveEventAuthorityPDA()
accounts = append(accounts,
&solana.AccountMeta{PublicKey: eventAuthority, IsWritable: false, IsSigner: false},
&solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false},
)

return accounts
}

Expand Down
46 changes: 39 additions & 7 deletions universalClient/externalchains/svm/tx_builder_pc20.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,17 @@ func (tb *TxBuilder) buildPC20ExportAccounts(
accounts = append(accounts, &solana.AccountMeta{PublicKey: storeRefundRecipient, IsWritable: true, IsSigner: false})
}

// #[event_cpi] accounts: required, at the same named-account boundary as the
// non-PC20 finalize route, so the gateway can self-CPI its emit_cpi event.
eventAuthority, err := tb.deriveEventAuthorityPDA()
if err != nil {
return nil, fmt.Errorf("failed to derive event_authority PDA: %w", err)
}
accounts = append(accounts,
&solana.AccountMeta{PublicKey: eventAuthority, IsWritable: false, IsSigner: false},
&solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false},
)

// Remaining accounts: [pc20_state, pc20_mint] + the payload accounts (only when
// user_data is present). The gateway requires exactly these two when user_data is
// empty — no recipient_ata, since the wrapper is minted to cea_ata.
Expand All @@ -775,7 +786,12 @@ func (tb *TxBuilder) buildPC20ExportAccounts(

// buildPC20RemintAccounts builds the revert_universal_tx / rescue_funds account list for
// the PC20 remint branch: token_vault + recipient_token_account None, token_mint + token_program
// present, remaining = [pc20_state, pc20_mint(w), recipient_ata(w), ATA_program, rent].
// present, associated_token_program + rent None (unused: the remaining accounts below carry
// their own copies for creating recipient_ata), then the #[event_cpi] pair, then
// remaining = [pc20_state, pc20_mint(w), recipient_ata(w), ATA_program, rent].
//
// Must match the same RevertUniversalTx struct as buildRevertAccounts — PC20 remint
// dispatches through the same instruction, so positions 1-14 have to line up with it.
func (tb *TxBuilder) buildPC20RemintAccounts(
configPDA solana.PublicKey,
vaultPDA solana.PublicKey,
Expand Down Expand Up @@ -812,12 +828,28 @@ func (tb *TxBuilder) buildPC20RemintAccounts(
none, // recipient_token_account
{PublicKey: mint, IsWritable: false, IsSigner: false},
{PublicKey: solana.TokenProgramID, IsWritable: false, IsSigner: false},
// remaining accounts — exact shape required by is_pc20_remint_account_shape
{PublicKey: pc20State, IsWritable: false, IsSigner: false},
{PublicKey: mint, IsWritable: true, IsSigner: false},
{PublicKey: recipientATA, IsWritable: true, IsSigner: false},
{PublicKey: solana.SPLAssociatedTokenAccountProgramID, IsWritable: false, IsSigner: false},
{PublicKey: solana.SysVarRentPubkey, IsWritable: false, IsSigner: false},
none, // associated_token_program
none, // rent
}

// #[event_cpi] accounts: required so the gateway can self-CPI its emit_cpi
// event, at the same named-account boundary buildRevertAccounts uses.
eventAuthority, err := tb.deriveEventAuthorityPDA()
if err != nil {
return nil, fmt.Errorf("failed to derive event_authority PDA: %w", err)
}
accounts = append(accounts,
&solana.AccountMeta{PublicKey: eventAuthority, IsWritable: false, IsSigner: false},
&solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false},
)

// remaining accounts — exact shape required by is_pc20_remint_account_shape
accounts = append(accounts,
&solana.AccountMeta{PublicKey: pc20State, IsWritable: false, IsSigner: false},
&solana.AccountMeta{PublicKey: mint, IsWritable: true, IsSigner: false},
&solana.AccountMeta{PublicKey: recipientATA, IsWritable: true, IsSigner: false},
&solana.AccountMeta{PublicKey: solana.SPLAssociatedTokenAccountProgramID, IsWritable: false, IsSigner: false},
&solana.AccountMeta{PublicKey: solana.SysVarRentPubkey, IsWritable: false, IsSigner: false},
)
return accounts, nil
}
73 changes: 48 additions & 25 deletions universalClient/externalchains/svm/tx_builder_pc20_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ func TestBuildPC20ExportAccounts_Direct(t *testing.T) {
)
require.NoError(t, err)

// 20 typed slots + [pc20_state, pc20_mint] (export-only mints to cea_ata, so no
// recipient_ata in the remaining accounts).
require.Len(t, accounts, 22)
// 20 typed slots + event_cpi(2) + [pc20_state, pc20_mint] (export-only mints to
// cea_ata, so no recipient_ata in the remaining accounts).
require.Len(t, accounts, 24)

assert.Equal(t, caller, accounts[0].PublicKey)
assert.True(t, accounts[0].IsSigner)
Expand All @@ -236,11 +236,17 @@ func TestBuildPC20ExportAccounts_Direct(t *testing.T) {
assert.Equal(t, solana.SysVarRentPubkey, accounts[13].PublicKey)
assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, accounts[14].PublicKey)

// event_cpi accounts (20-21)
wantEventAuthority, err := tb.deriveEventAuthorityPDA()
require.NoError(t, err)
assert.Equal(t, wantEventAuthority, accounts[20].PublicKey, "event_authority")
assert.Equal(t, tb.gatewayAddress, accounts[21].PublicKey, "program")

// Remaining accounts: [pc20_state, pc20_mint]
assert.Equal(t, state, accounts[20].PublicKey)
assert.True(t, accounts[20].IsWritable)
assert.Equal(t, mint, accounts[21].PublicKey)
assert.True(t, accounts[21].IsWritable)
assert.Equal(t, state, accounts[22].PublicKey)
assert.True(t, accounts[22].IsWritable)
assert.Equal(t, mint, accounts[23].PublicKey)
assert.True(t, accounts[23].IsWritable)
}

func TestBuildPC20ExportAccounts_WithPayload(t *testing.T) {
Expand All @@ -267,8 +273,8 @@ func TestBuildPC20ExportAccounts_WithPayload(t *testing.T) {
)
require.NoError(t, err)

// 20 typed slots + [pc20_state, pc20_mint] + 1 payload account
require.Len(t, accounts, 23)
// 20 typed slots + event_cpi(2) + [pc20_state, pc20_mint] + 1 payload account
require.Len(t, accounts, 25)
assert.Equal(t, target, accounts[7].PublicKey) // destination_program = payload target

// cea_ata (slot 10) is the CEA's ATA, writable
Expand All @@ -279,11 +285,17 @@ func TestBuildPC20ExportAccounts_WithPayload(t *testing.T) {
assert.Equal(t, expectedCeaATA, accounts[10].PublicKey)
assert.True(t, accounts[10].IsWritable)

// event_cpi accounts (20-21)
wantEventAuthority, err := tb.deriveEventAuthorityPDA()
require.NoError(t, err)
assert.Equal(t, wantEventAuthority, accounts[20].PublicKey, "event_authority")
assert.Equal(t, tb.gatewayAddress, accounts[21].PublicKey, "program")

// remaining: state, mint, then payload accounts (no recipient_ata)
assert.Equal(t, state, accounts[20].PublicKey)
assert.Equal(t, mint, accounts[21].PublicKey)
assert.Equal(t, recipient, accounts[22].PublicKey)
assert.True(t, accounts[22].IsWritable)
assert.Equal(t, state, accounts[22].PublicKey)
assert.Equal(t, mint, accounts[23].PublicKey)
assert.Equal(t, recipient, accounts[24].PublicKey)
assert.True(t, accounts[24].IsWritable)
}

func TestBuildPC20RemintAccounts(t *testing.T) {
Expand All @@ -308,8 +320,8 @@ func TestBuildPC20RemintAccounts(t *testing.T) {
)
require.NoError(t, err)

// 12 typed slots + 5 remaining
require.Len(t, accounts, 17)
// 12 typed slots + associated_token_program/rent None sentinels(2) + event_cpi(2) + 5 remaining
require.Len(t, accounts, 21)

assert.Equal(t, configPDA, accounts[0].PublicKey)
assert.Equal(t, recipient, accounts[4].PublicKey)
Expand All @@ -322,21 +334,32 @@ func TestBuildPC20RemintAccounts(t *testing.T) {
assert.Equal(t, mint, accounts[10].PublicKey)
assert.Equal(t, solana.TokenProgramID, accounts[11].PublicKey)

// associated_token_program(12) and rent(13) are also None sentinels — PC20 remint
// creates recipient_ata via the remaining accounts below, not this typed slot.
assert.Equal(t, tb.gatewayAddress, accounts[12].PublicKey)
assert.Equal(t, tb.gatewayAddress, accounts[13].PublicKey)

// event_cpi accounts (14-15)
wantEventAuthority, err := tb.deriveEventAuthorityPDA()
require.NoError(t, err)
assert.Equal(t, wantEventAuthority, accounts[14].PublicKey, "event_authority")
assert.Equal(t, tb.gatewayAddress, accounts[15].PublicKey, "program")

// remaining: [pc20_state(ro), pc20_mint(w), recipient_ata(w), ATA program(ro), rent(ro)]
assert.Equal(t, state, accounts[12].PublicKey)
assert.False(t, accounts[12].IsWritable)
assert.Equal(t, mint, accounts[13].PublicKey)
assert.True(t, accounts[13].IsWritable)
assert.Equal(t, state, accounts[16].PublicKey)
assert.False(t, accounts[16].IsWritable)
assert.Equal(t, mint, accounts[17].PublicKey)
assert.True(t, accounts[17].IsWritable)
expectedATA, _, _ := solana.FindProgramAddress(
[][]byte{recipient.Bytes(), solana.TokenProgramID.Bytes(), mint.Bytes()},
solana.SPLAssociatedTokenAccountProgramID,
)
assert.Equal(t, expectedATA, accounts[14].PublicKey)
assert.True(t, accounts[14].IsWritable)
assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, accounts[15].PublicKey)
assert.False(t, accounts[15].IsWritable)
assert.Equal(t, solana.SysVarRentPubkey, accounts[16].PublicKey)
assert.False(t, accounts[16].IsWritable)
assert.Equal(t, expectedATA, accounts[18].PublicKey)
assert.True(t, accounts[18].IsWritable)
assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, accounts[19].PublicKey)
assert.False(t, accounts[19].IsWritable)
assert.Equal(t, solana.SysVarRentPubkey, accounts[20].PublicKey)
assert.False(t, accounts[20].IsWritable)
}

func TestValidatePC20UserData(t *testing.T) {
Expand Down
Loading
Loading