Function: rescue_funds
Direction: Vault → Recipient
Authorization: TSS ECDSA secp256k1 signature
instruction_id: 4 (both SOL and SPL)
Emergency release of locked funds when normal outbound paths (withdraw/execute/revert) cannot be used. Authorized exclusively by TSS. Replay-protected via sub_tx_id.
Rescue is for funds that are permanently locked — e.g., the original deposit's revert_recipient is invalid or the associated outbound transaction can never be finalized. TSS initiates rescue off-chain; it is not triggered by user request.
Rescue is distinct from revert:
- Revert (3): standard recovery of a failed Push Chain transaction; uses same
revert_recipientas original deposit - Rescue (4): TSS-authorized emergency release to any recipient; used when normal recovery paths are unavailable
- Validate account presence (SOL vs SPL paths)
- Verify TSS signature — recover Ethereum address, compare to
TssPda.tss_eth_address - Create
ExecutedSubTxPDA (replay protection — init fails ifsub_tx_idreused) Vault → Recipient(amount)- Emit
FundsRescued FeeVault → Caller(gas_fee, UV reimbursement)
The funds transfer comes from the bridge Vault. The UV reimbursement comes from FeeVault — not from Vault. This preserves the 1:1 bridge invariant. If FeeVault has insufficient balance, reimbursement fails with InsufficientFeePool.
PREFIX = b"PUSH_CHAIN_SVM"
message = PREFIX || instruction_id (1 byte) || chain_id || amount (8 bytes BE) || additional_data
hash = keccak256(message)
sub_tx_id[32] | universal_tx_id[32] | recipient[32] | gas_fee (8 BE)
sub_tx_id[32] | universal_tx_id[32] | mint[32] | recipient[32] | gas_fee (8 BE)
PREFIX = b"PUSH_CHAIN_SVM"
Reference: buildRescueAdditionalData() in tests/helpers/tss.ts
| Account | SOL route | SPL route |
|---|---|---|
config |
Required | Required |
vault |
Required | Required |
fee_vault |
Required | Required |
tss_pda |
Required | Required |
recipient |
Required | Required (wallet, not ATA) |
executed_sub_tx |
Required (created) | Required (created) |
caller |
Required (signer) | Required (signer) |
system_program |
Required | Required |
token_vault |
None | Required (vault ATA for mint) |
recipient_token_account |
None | Required (must exist) |
token_mint |
None | Required |
token_program |
None | Required |
For SOL, pass token_vault, recipient_token_account, token_mint, token_program as null.
Cross-account constraints (SPL):
token_vault.mint == token_mint.key()token_vault.owner == vault.key()recipient_token_account.mint == token_mint.key()recipient_token_account.owner == recipient.key()
The recipient account in the TSS message is the wallet pubkey (owner), not the ATA. The recipient ATA must already exist — rescue does not create it.
ExecutedSubTx PDA seeded by ["executed_sub_tx", sub_tx_id] is created on execution. Anchor's init constraint causes a second call with the same sub_tx_id to fail at account creation — the same mechanism used by withdraw, execute, and revert.
FundsRescued {
sub_tx_id: [u8; 32],
universal_tx_id: [u8; 32],
token: Pubkey, // Pubkey::default() for SOL, mint for SPL
amount: u64,
revert_instruction: RevertInstructions {
revert_recipient: Pubkey, // recipient
revert_msg: Vec<u8>, // always empty for rescue
},
}Emitted after UV gas reimbursement from FeeVault.
| Error | Cause |
|---|---|
TssAuthFailed |
Signature invalid or TSS address mismatch |
MessageHashMismatch |
Message reconstruction does not match provided hash |
| account init failure | sub_tx_id reused — ExecutedSubTx PDA already exists |
InvalidAmount |
amount == 0 |
InvalidRecipient |
Recipient is zero address |
InvalidAccount |
SPL accounts missing or inconsistent (null/non-null mismatch) |
InvalidMint |
ATA mint does not match token_mint |
InsufficientFeePool |
FeeVault balance < gas_fee |
Paused |
Gateway is paused |