chain_signatures/lib.rs
1#![doc = include_str!("../README.md")]
2#![allow(unexpected_cfgs)]
3
4pub mod evm;
5use anchor_lang::prelude::*;
6
7declare_id!("SigMcRMjKfnC7RDG5q4yUMZM1s5KJ9oYTPP4NmJRDRw");
8
9#[program]
10pub mod chain_signatures {
11 use super::*;
12
13 /// Initialize the program state.
14 ///
15 /// # Admin Only
16 ///
17 /// This instruction is restricted to program deployment and is **not intended
18 /// for application developers**. It can only be called once to set up the program.
19 ///
20 /// # Arguments
21 ///
22 /// * `signature_deposit` - Required deposit in lamports for signature requests
23 /// * `chain_id` - CAIP-2 chain identifier (e.g., "solana:mainnet")
24 ///
25 /// # Accounts
26 ///
27 /// * `program_state` - PDA to store program configuration
28 /// * `admin` - Admin account (becomes program admin)
29 pub fn initialize(
30 ctx: Context<Initialize>,
31 signature_deposit: u64,
32 chain_id: String,
33 ) -> Result<()> {
34 let program_state = &mut ctx.accounts.program_state;
35 program_state.admin = ctx.accounts.admin.key();
36 program_state.signature_deposit = signature_deposit;
37 program_state.chain_id = chain_id;
38
39 Ok(())
40 }
41
42 /// Update the required signature deposit amount.
43 ///
44 /// # Admin Only
45 ///
46 /// This instruction is restricted to the program administrator and is **not intended
47 /// for application developers**. It is used for program maintenance.
48 ///
49 /// # Arguments
50 ///
51 /// * `new_deposit` - New deposit amount in lamports
52 ///
53 /// # Emits
54 ///
55 /// * [`DepositUpdatedEvent`]
56 pub fn update_deposit(ctx: Context<AdminOnly>, new_deposit: u64) -> Result<()> {
57 let program_state = &mut ctx.accounts.program_state;
58 let old_deposit = program_state.signature_deposit;
59 program_state.signature_deposit = new_deposit;
60
61 emit!(DepositUpdatedEvent {
62 old_deposit,
63 new_deposit,
64 });
65
66 Ok(())
67 }
68
69 /// Withdraw accumulated funds from the program.
70 ///
71 /// # Admin Only
72 ///
73 /// This instruction is restricted to the program administrator and is **not intended
74 /// for application developers**. It is used for program maintenance.
75 ///
76 /// # Arguments
77 ///
78 /// * `amount` - Amount to withdraw in lamports
79 ///
80 /// # Errors
81 ///
82 /// * [`ChainSignaturesError::InsufficientFunds`] - Program has insufficient balance
83 /// * [`ChainSignaturesError::InvalidRecipient`] - Recipient is zero address
84 ///
85 /// # Emits
86 ///
87 /// * [`FundsWithdrawnEvent`]
88 pub fn withdraw_funds(ctx: Context<WithdrawFunds>, amount: u64) -> Result<()> {
89 let program_state = &ctx.accounts.program_state;
90 let recipient = &ctx.accounts.recipient;
91
92 let program_state_info = program_state.to_account_info();
93 require!(
94 program_state_info.lamports() >= amount,
95 ChainSignaturesError::InsufficientFunds
96 );
97
98 require!(
99 recipient.key() != Pubkey::default(),
100 ChainSignaturesError::InvalidRecipient
101 );
102
103 // Transfer funds from program_state to recipient
104 **program_state_info.try_borrow_mut_lamports()? -= amount;
105 **recipient.try_borrow_mut_lamports()? += amount;
106
107 emit!(FundsWithdrawnEvent {
108 amount,
109 recipient: recipient.key(),
110 });
111
112 Ok(())
113 }
114
115 /// Request a signature from the MPC network on a 32-byte payload.
116 ///
117 /// The payload is typically a transaction hash that needs to be signed.
118 /// The MPC network will respond with a signature via [`respond`].
119 ///
120 /// # Arguments
121 ///
122 /// * `payload` - 32-byte data to sign (typically a transaction hash)
123 /// * `key_version` - MPC key version to use
124 /// * `path` - Derivation path for the user's key (e.g., `"my_wallet"`)
125 /// * `algo` - Reserved for future use (pass empty string `""`)
126 /// * `dest` - Reserved for future use (pass empty string `""`)
127 /// * `params` - Reserved for future use (pass empty string `""`)
128 ///
129 /// # Emits
130 ///
131 /// * [`SignatureRequestedEvent`]
132 ///
133 /// # Example
134 ///
135 /// ```typescript,ignore
136 /// await program.methods
137 /// .sign(
138 /// Array.from(txHash), // [u8; 32] payload to sign
139 /// 0, // key_version
140 /// "my_wallet", // path (derivation path)
141 /// "", // algo (reserved, pass empty string)
142 /// "", // dest (reserved, pass empty string)
143 /// "" // params (reserved, pass empty string)
144 /// )
145 /// .accounts({ ... })
146 /// .rpc();
147 /// ```
148 pub fn sign(
149 ctx: Context<Sign>,
150 payload: [u8; 32],
151 key_version: u32,
152 path: String,
153 algo: String,
154 dest: String,
155 params: String,
156 ) -> Result<()> {
157 let program_state = &ctx.accounts.program_state;
158 let requester = &ctx.accounts.requester;
159 let system_program = &ctx.accounts.system_program;
160
161 let payer = match &ctx.accounts.fee_payer {
162 Some(fee_payer) => fee_payer.to_account_info(),
163 None => requester.to_account_info(),
164 };
165
166 require!(
167 payer.lamports() >= program_state.signature_deposit,
168 ChainSignaturesError::InsufficientDeposit
169 );
170
171 let transfer_instruction = anchor_lang::system_program::Transfer {
172 from: payer,
173 to: program_state.to_account_info(),
174 };
175
176 anchor_lang::system_program::transfer(
177 CpiContext::new(system_program.to_account_info(), transfer_instruction),
178 program_state.signature_deposit,
179 )?;
180
181 emit_cpi!(SignatureRequestedEvent {
182 sender: *requester.key,
183 payload,
184 key_version,
185 deposit: program_state.signature_deposit,
186 chain_id: program_state.chain_id.clone(),
187 path,
188 algo,
189 dest,
190 params,
191 fee_payer: match &ctx.accounts.fee_payer {
192 Some(payer) => Some(*payer.key),
193 None => None,
194 },
195 });
196
197 Ok(())
198 }
199
200 /// Initiate a bidirectional cross-chain transaction with execution result callback.
201 ///
202 /// This is the primary entry point for cross-chain transactions. The flow:
203 /// 1. User submits unsigned transaction → MPC signs and responds
204 /// 2. User broadcasts to destination chain
205 /// 3. MPC observes execution via light client
206 /// 4. MPC returns execution result via [`respond_bidirectional`]
207 ///
208 /// Chain-agnostic lifecycle reference: <https://docs.sig.network/architecture/sign-bidirectional>
209 ///
210 /// # Arguments
211 ///
212 /// * `serialized_transaction` - serialized unsigned transaction for destination chain
213 /// * `caip2_id` - CAIP-2 chain identifier (e.g., `"eip155:1"` for Ethereum mainnet)
214 /// * `key_version` - MPC key version to use
215 /// * `path` - Derivation path for signing key
216 /// * `algo` - Reserved for future use (pass empty string `""`)
217 /// * `dest` - Reserved for future use (pass empty string `""`)
218 /// * `params` - Reserved for future use (pass empty string `""`)
219 /// * `program_id` - Callback program ID (reserved for future use)
220 /// * `output_deserialization_schema` - serialization schema for parsing destination chain output
221 /// * `respond_serialization_schema` - serialization schema for serializing response to source chain
222 ///
223 /// # Emits
224 ///
225 /// * [`SignBidirectionalEvent`]
226 ///
227 /// # Errors
228 ///
229 /// * [`ChainSignaturesError::InvalidTransaction`] - Empty transaction data
230 /// * [`ChainSignaturesError::InsufficientDeposit`] - Insufficient deposit
231 pub fn sign_bidirectional(
232 ctx: Context<SignBidirectional>,
233 serialized_transaction: Vec<u8>,
234 caip2_id: String,
235 key_version: u32,
236 path: String,
237 algo: String,
238 dest: String,
239 params: String,
240 program_id: Pubkey,
241 output_deserialization_schema: Vec<u8>,
242 respond_serialization_schema: Vec<u8>,
243 ) -> Result<()> {
244 let program_state = &ctx.accounts.program_state;
245 let requester = &ctx.accounts.requester;
246 let system_program = &ctx.accounts.system_program;
247
248 let payer = match &ctx.accounts.fee_payer {
249 Some(fee_payer) => fee_payer.to_account_info(),
250 None => requester.to_account_info(),
251 };
252
253 require!(
254 payer.lamports() >= program_state.signature_deposit,
255 ChainSignaturesError::InsufficientDeposit
256 );
257
258 require!(
259 !serialized_transaction.is_empty(),
260 ChainSignaturesError::InvalidTransaction
261 );
262
263 let transfer_instruction = anchor_lang::system_program::Transfer {
264 from: payer,
265 to: program_state.to_account_info(),
266 };
267
268 anchor_lang::system_program::transfer(
269 CpiContext::new(system_program.to_account_info(), transfer_instruction),
270 program_state.signature_deposit,
271 )?;
272
273 emit_cpi!(SignBidirectionalEvent {
274 sender: *requester.key,
275 serialized_transaction,
276 caip2_id,
277 key_version,
278 deposit: program_state.signature_deposit,
279 path,
280 algo,
281 dest,
282 params,
283 program_id,
284 output_deserialization_schema,
285 respond_serialization_schema
286 });
287
288 Ok(())
289 }
290
291 /// Respond to signature requests with generated signatures.
292 ///
293 /// Called by MPC responders after signature generation. Supports batched
294 /// requests where each signature is linked to its request via `request_id`.
295 ///
296 /// # Security Note
297 ///
298 /// **Any address can call this function.** Clients must verify signature
299 /// validity off-chain before trusting the response.
300 ///
301 /// # Arguments
302 ///
303 /// * `request_ids` - Array of 32-byte request identifiers
304 /// * `signatures` - Corresponding ECDSA signatures
305 ///
306 /// # Emits
307 ///
308 /// * [`SignatureRespondedEvent`] for each signature
309 pub fn respond(
310 ctx: Context<Respond>,
311 request_ids: Vec<[u8; 32]>,
312 signatures: Vec<Signature>,
313 ) -> Result<()> {
314 require!(
315 request_ids.len() == signatures.len(),
316 ChainSignaturesError::InvalidInputLength
317 );
318
319 for i in 0..request_ids.len() {
320 emit_cpi!(SignatureRespondedEvent {
321 request_id: request_ids[i],
322 responder: *ctx.accounts.responder.key,
323 signature: signatures[i].clone(),
324 });
325 }
326
327 Ok(())
328 }
329
330 /// Report signature generation errors from the MPC network.
331 ///
332 /// # Warning: Debugging Only
333 ///
334 /// This function is **solely for debugging purposes** and should not be used
335 /// in production or relied upon for any business logic. Error events are
336 /// informational only and are not cryptographically verified.
337 ///
338 /// # Security Note
339 ///
340 /// **Any address can call this function.** Do not rely on error events
341 /// for business logic decisions.
342 ///
343 /// # Arguments
344 ///
345 /// * `errors` - Array of error responses with request IDs and messages
346 ///
347 /// # Emits
348 ///
349 /// * [`SignatureErrorEvent`] for each error
350 pub fn respond_error(ctx: Context<RespondError>, errors: Vec<ErrorResponse>) -> Result<()> {
351 for error in errors {
352 emit!(SignatureErrorEvent {
353 request_id: error.request_id,
354 responder: *ctx.accounts.responder.key,
355 error: error.error_message,
356 });
357 }
358
359 Ok(())
360 }
361
362 /// Get the current signature deposit amount. View function.
363 ///
364 /// # Returns
365 ///
366 /// Current signature deposit in lamports.
367 pub fn get_signature_deposit(ctx: Context<GetSignatureDeposit>) -> Result<u64> {
368 let program_state = &ctx.accounts.program_state;
369 Ok(program_state.signature_deposit)
370 }
371
372 /// Finalize a bidirectional flow with execution results from the destination chain.
373 ///
374 /// Called by MPC responders after observing transaction confirmation on the
375 /// destination chain. The signature proves the authenticity of the output.
376 ///
377 /// # Arguments
378 ///
379 /// * `request_id` - Original 32-byte request identifier
380 /// * `serialized_output` - Serialized execution output per `respond_serialization_schema`
381 /// * `signature` - ECDSA signature over `keccak256(request_id || serialized_output)`
382 ///
383 /// # Output Format
384 ///
385 /// For **successful transactions**:
386 /// - Contract call: Serialized return value per schema
387 /// - Simple transfer: Empty success indicator
388 ///
389 /// For **failed transactions**:
390 /// - Magic prefix `0xdeadbeef` followed by failure indicator
391 ///
392 /// # Emits
393 ///
394 /// * [`RespondBidirectionalEvent`]
395 pub fn respond_bidirectional(
396 ctx: Context<ReadRespond>,
397 request_id: [u8; 32],
398 serialized_output: Vec<u8>,
399 signature: Signature,
400 ) -> Result<()> {
401 emit!(RespondBidirectionalEvent {
402 request_id,
403 responder: *ctx.accounts.responder.key,
404 serialized_output,
405 signature,
406 });
407
408 Ok(())
409 }
410}
411
412/// Program configuration state stored in a PDA.
413///
414/// Seeds: `[b"program-state"]`
415#[account]
416pub struct ProgramState {
417 /// Admin account with permission to update settings and withdraw funds.
418 pub admin: Pubkey,
419 /// Required deposit in lamports for signature requests.
420 pub signature_deposit: u64,
421 /// CAIP-2 chain identifier (e.g., "solana:mainnet").
422 pub chain_id: String,
423}
424
425/// A point on the secp256k1 elliptic curve in affine coordinates.
426///
427/// Used to represent the R point in ECDSA signatures.
428///
429/// # Size
430///
431/// 64 bytes total (32 bytes x + 32 bytes y)
432#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
433pub struct AffinePoint {
434 /// X coordinate (big-endian, 32 bytes)
435 pub x: [u8; 32],
436 /// Y coordinate (big-endian, 32 bytes)
437 pub y: [u8; 32],
438}
439
440/// ECDSA signature in affine point representation.
441///
442/// Compatible with secp256k1 curve operations. Can be converted to
443/// standard RSV format for use with Ethereum and other chains.
444///
445/// # Size
446///
447/// 97 bytes total (64 bytes big_r + 32 bytes s + 1 byte recovery_id)
448///
449/// # Conversion to RSV
450///
451/// ```typescript,ignore
452/// const r = Buffer.from(signature.bigR.x).toString('hex');
453/// const s = Buffer.from(signature.s).toString('hex');
454/// const v = signature.recoveryId + 27;
455/// ```
456#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
457pub struct Signature {
458 /// R point of the ECDSA signature (affine coordinates)
459 pub big_r: AffinePoint,
460 /// s scalar of the signature (32 bytes, big-endian)
461 pub s: [u8; 32],
462 /// Recovery ID (0 or 1) for public key recovery
463 pub recovery_id: u8,
464}
465
466/// Error information for failed signature requests.
467#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
468pub struct ErrorResponse {
469 /// Identifier of the failed request
470 pub request_id: [u8; 32],
471 /// Human-readable error description
472 pub error_message: String,
473}
474
475#[derive(Accounts)]
476pub struct Initialize<'info> {
477 #[account(
478 init,
479 payer = admin,
480 space = 8 + 32 + 8 + 4 + 128, // discriminator + admin + deposit + string length + max chain_id length
481 seeds = [b"program-state"],
482 bump
483 )]
484 pub program_state: Account<'info, ProgramState>,
485 #[account(mut)]
486 pub admin: Signer<'info>,
487 pub system_program: Program<'info, System>,
488}
489
490#[derive(Accounts)]
491pub struct AdminOnly<'info> {
492 #[account(
493 mut,
494 seeds = [b"program-state"],
495 bump,
496 has_one = admin @ ChainSignaturesError::Unauthorized
497 )]
498 pub program_state: Account<'info, ProgramState>,
499 #[account(mut)]
500 pub admin: Signer<'info>,
501 pub system_program: Program<'info, System>,
502}
503
504#[derive(Accounts)]
505pub struct WithdrawFunds<'info> {
506 #[account(
507 mut,
508 seeds = [b"program-state"],
509 bump,
510 has_one = admin @ ChainSignaturesError::Unauthorized
511 )]
512 pub program_state: Account<'info, ProgramState>,
513
514 #[account(mut)]
515 pub admin: Signer<'info>,
516
517 /// CHECK: The safety check is performed in the withdraw_funds
518 /// function by checking it is not the zero address.
519 #[account(mut)]
520 pub recipient: AccountInfo<'info>,
521
522 pub system_program: Program<'info, System>,
523}
524
525#[event_cpi]
526#[derive(Accounts)]
527pub struct Sign<'info> {
528 #[account(mut, seeds = [b"program-state"], bump)]
529 pub program_state: Account<'info, ProgramState>,
530 #[account(mut)]
531 pub requester: Signer<'info>,
532 #[account(mut)]
533 pub fee_payer: Option<Signer<'info>>,
534 pub system_program: Program<'info, System>,
535}
536
537#[event_cpi]
538#[derive(Accounts)]
539pub struct SignBidirectional<'info> {
540 #[account(mut, seeds = [b"program-state"], bump)]
541 pub program_state: Account<'info, ProgramState>,
542 #[account(mut)]
543 pub requester: Signer<'info>,
544 #[account(mut)]
545 pub fee_payer: Option<Signer<'info>>,
546 pub system_program: Program<'info, System>,
547 pub instructions: Option<AccountInfo<'info>>,
548}
549
550#[event_cpi]
551#[derive(Accounts)]
552pub struct Respond<'info> {
553 pub responder: Signer<'info>,
554}
555
556#[derive(Accounts)]
557pub struct RespondError<'info> {
558 pub responder: Signer<'info>,
559}
560
561#[derive(Accounts)]
562pub struct GetSignatureDeposit<'info> {
563 #[account(seeds = [b"program-state"], bump)]
564 pub program_state: Account<'info, ProgramState>,
565}
566
567#[derive(Accounts)]
568pub struct ReadRespond<'info> {
569 pub responder: Signer<'info>,
570}
571
572/// Emitted when a signature is requested via the [`chain_signatures::sign`] instruction.
573///
574/// # Event Type
575///
576/// CPI event (emitted via `emit_cpi!`)
577#[event]
578pub struct SignatureRequestedEvent {
579 /// Solana address of the requester.
580 pub sender: Pubkey,
581 /// 32-byte payload to be signed (typically a transaction hash).
582 pub payload: [u8; 32],
583 /// MPC key version used for signing.
584 pub key_version: u32,
585 /// Deposit amount paid in lamports.
586 pub deposit: u64,
587 /// CAIP-2 chain identifier of this program (e.g., "solana:mainnet").
588 pub chain_id: String,
589 /// Derivation path for the user's signing key.
590 pub path: String,
591 /// Signing algorithm (e.g., "secp256k1").
592 pub algo: String,
593 /// Response destination chain.
594 pub dest: String,
595 /// Additional JSON parameters.
596 pub params: String,
597 /// Optional separate fee payer account.
598 pub fee_payer: Option<Pubkey>,
599}
600
601/// Emitted when a bidirectional cross-chain request is made via
602/// [`chain_signatures::sign_bidirectional`].
603///
604/// # Event Type
605///
606/// CPI event (emitted via `emit_cpi!`)
607///
608/// # Usage
609///
610/// The MPC network listens for this event to:
611/// 1. Sign the transaction and call [`chain_signatures::respond`]
612/// 2. Store the pending tx in backlog for observation
613/// 3. Monitor destination chain for confirmation
614/// 4. Call [`chain_signatures::respond_bidirectional`] with results
615#[event]
616pub struct SignBidirectionalEvent {
617 /// Solana address of the requester.
618 pub sender: Pubkey,
619 /// RLP-encoded unsigned transaction for the destination chain.
620 pub serialized_transaction: Vec<u8>,
621 /// CAIP-2 chain identifier of the destination (e.g., "eip155:1" for Ethereum).
622 pub caip2_id: String,
623 /// MPC key version used for signing.
624 pub key_version: u32,
625 /// Deposit amount paid in lamports.
626 pub deposit: u64,
627 /// Derivation path for the user's signing key.
628 pub path: String,
629 /// Signing algorithm (e.g., "secp256k1").
630 pub algo: String,
631 /// Response destination identifier.
632 pub dest: String,
633 /// Additional JSON parameters.
634 pub params: String,
635 /// Callback program ID (reserved for future use).
636 pub program_id: Pubkey,
637 /// Schema for parsing destination chain call output (JSON-encoded).
638 pub output_deserialization_schema: Vec<u8>,
639 /// Schema for serializing response to source chain (JSON-encoded).
640 pub respond_serialization_schema: Vec<u8>,
641}
642
643/// Emitted when the MPC network returns a signature via [`chain_signatures::respond`].
644///
645/// # Event Type
646///
647/// CPI event (emitted via `emit_cpi!`)
648///
649/// # Security Warning
650///
651/// **Any address can emit this event.** Clients **must** verify signature validity
652/// by recovering the public key and comparing with the expected derived key.
653#[event]
654pub struct SignatureRespondedEvent {
655 /// Request identifier linking this response to the original request.
656 /// Computed as `keccak256(sender || payload || ...)` - see module docs.
657 pub request_id: [u8; 32],
658 /// Address of the responder. Clients must verify the signature was produced by the MPC.
659 pub responder: Pubkey,
660 /// ECDSA signature in affine point format.
661 pub signature: Signature,
662}
663
664/// Emitted when signature generation fails via [`chain_signatures::respond_error`].
665///
666/// # Warning: Debugging Only
667///
668/// This event is **solely for debugging purposes**. Do not use in production
669/// or rely upon for any business logic.
670///
671/// # Event Type
672///
673/// Regular event (emitted via `emit!`)
674///
675/// # Security Warning
676///
677/// **Any address can emit this event.** Error events are not cryptographically
678/// verified and should never be trusted for business logic decisions.
679#[event]
680pub struct SignatureErrorEvent {
681 /// Request identifier of the failed request.
682 pub request_id: [u8; 32],
683 /// Address of the MPC responder. Error events are not cryptographically verified.
684 pub responder: Pubkey,
685 /// Human-readable error description.
686 pub error: String,
687}
688
689/// Emitted when the MPC network returns execution results for a bidirectional
690/// request via [`chain_signatures::respond_bidirectional`].
691///
692/// # Event Type
693///
694/// Regular event (emitted via `emit!`)
695///
696/// # Output Format
697///
698/// **Successful transactions:**
699/// - Contract call: Return value serialized per `respond_serialization_schema`
700/// - Simple transfer: Empty success indicator
701///
702/// **Failed transactions:**
703/// - Magic prefix `0xdeadbeef` followed by failure indicator
704///
705/// # Signature Verification
706///
707/// The signature is computed over `keccak256(request_id || serialized_output)` using
708/// the special derivation path `"solana response key"`. See module-level docs for
709/// verification procedure.
710///
711/// # Security Warning
712///
713/// **Any address can emit this event.** Clients **must** verify the signature
714/// before trusting the output.
715#[event]
716pub struct RespondBidirectionalEvent {
717 /// Original request identifier.
718 pub request_id: [u8; 32],
719 /// Address of the MPC responder. Clients must verify the signature was produced by the MPC.
720 pub responder: Pubkey,
721 /// Serialized execution output per `respond_serialization_schema`.
722 /// Check for `0xdeadbeef` prefix to detect failures.
723 pub serialized_output: Vec<u8>,
724 /// ECDSA signature over `keccak256(request_id || serialized_output)`.
725 pub signature: Signature,
726}
727
728/// Emitted when the admin updates the signature deposit via
729/// [`chain_signatures::update_deposit`].
730#[event]
731pub struct DepositUpdatedEvent {
732 /// Previous deposit amount in lamports.
733 pub old_deposit: u64,
734 /// New deposit amount in lamports.
735 pub new_deposit: u64,
736}
737
738/// Emitted when the admin withdraws funds via [`chain_signatures::withdraw_funds`].
739#[event]
740pub struct FundsWithdrawnEvent {
741 /// Amount withdrawn in lamports.
742 pub amount: u64,
743 /// Recipient address.
744 pub recipient: Pubkey,
745}
746
747#[error_code]
748pub enum ChainSignaturesError {
749 #[msg("Insufficient deposit amount")]
750 InsufficientDeposit,
751 #[msg("Arrays must have the same length")]
752 InvalidInputLength,
753 #[msg("Unauthorized access")]
754 Unauthorized,
755 #[msg("Insufficient funds for withdrawal")]
756 InsufficientFunds,
757 #[msg("Invalid recipient address")]
758 InvalidRecipient,
759 #[msg("Invalid transaction data")]
760 InvalidTransaction,
761 #[msg("Missing instruction sysvar")]
762 MissingInstructionSysvar,
763}