From ef7cdbadb95479dfa639eaae17e0770596924214 Mon Sep 17 00:00:00 2001 From: aalu1418 <50029043+aalu1418@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:56:43 -0700 Subject: [PATCH] example-ccip-sender framework --- chains/solana/contracts/Anchor.toml | 1 + chains/solana/contracts/Cargo.lock | 10 +- .../programs/example-ccip-receiver/Cargo.toml | 1 - .../programs/example-ccip-sender/Cargo.toml | 21 ++ .../programs/example-ccip-sender/Xargo.toml | 2 + .../programs/example-ccip-sender/src/lib.rs | 325 ++++++++++++++++++ 6 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 chains/solana/contracts/programs/example-ccip-sender/Cargo.toml create mode 100644 chains/solana/contracts/programs/example-ccip-sender/Xargo.toml create mode 100644 chains/solana/contracts/programs/example-ccip-sender/src/lib.rs diff --git a/chains/solana/contracts/Anchor.toml b/chains/solana/contracts/Anchor.toml index f262bf879..65749c4e5 100644 --- a/chains/solana/contracts/Anchor.toml +++ b/chains/solana/contracts/Anchor.toml @@ -28,6 +28,7 @@ ccip_router = "C8WSPj3yyus1YN3yNB6YA5zStYtbjQWtpmKadmvyUXq8" mcm = "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX" timelock = "LoCoNsJFuhTkSQjfdDfn3yuwqhSYoPujmviRHVCzsqn" token_pool = "GRvFSLwR7szpjgNEZbGe4HtxfJYXqySXuuRUAJDpu4WH" +example_ccip_sender = "CcipSender111111111111111111111111111111111" example_ccip_receiver = "CcipReceiver1111111111111111111111111111111" test_ccip_receiver = "CtEVnHsQzhTNWav8skikiV2oF6Xx7r7uGGa8eCDQtTjH" test_ccip_invalid_receiver = "9Vjda3WU2gsJgE4VdU6QuDw8rfHLyigfFyWs3XDPNUn8" diff --git a/chains/solana/contracts/Cargo.lock b/chains/solana/contracts/Cargo.lock index 5ae635710..dea5cc85e 100644 --- a/chains/solana/contracts/Cargo.lock +++ b/chains/solana/contracts/Cargo.lock @@ -201,7 +201,6 @@ dependencies = [ "anchor-derive-accounts", "anchor-derive-serde", "anchor-derive-space", - "anchor-syn", "arrayref", "base64 0.13.1", "bincode", @@ -944,6 +943,15 @@ dependencies = [ "solana-program", ] +[[package]] +name = "example_ccip_sender" +version = "0.1.0-dev" +dependencies = [ + "anchor-lang", + "anchor-spl", + "solana-program", +] + [[package]] name = "external-program-cpi-stub" version = "0.0.0-dev" diff --git a/chains/solana/contracts/programs/example-ccip-receiver/Cargo.toml b/chains/solana/contracts/programs/example-ccip-receiver/Cargo.toml index 9cb28282d..2661419ff 100644 --- a/chains/solana/contracts/programs/example-ccip-receiver/Cargo.toml +++ b/chains/solana/contracts/programs/example-ccip-receiver/Cargo.toml @@ -14,7 +14,6 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] -idel-build = ["anchor-lang/idl-build"] [dependencies] solana-program = "1.17.25" # pin solana to 1.17 diff --git a/chains/solana/contracts/programs/example-ccip-sender/Cargo.toml b/chains/solana/contracts/programs/example-ccip-sender/Cargo.toml new file mode 100644 index 000000000..e87fdeb7d --- /dev/null +++ b/chains/solana/contracts/programs/example-ccip-sender/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "example_ccip_sender" +version = "0.1.0-dev" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "example_ccip_sender" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +solana-program = "1.17.25" # pin solana to 1.17 +anchor-lang = { version = "0.29.0", features = [] } +anchor-spl = "0.29.0" diff --git a/chains/solana/contracts/programs/example-ccip-sender/Xargo.toml b/chains/solana/contracts/programs/example-ccip-sender/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/chains/solana/contracts/programs/example-ccip-sender/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/chains/solana/contracts/programs/example-ccip-sender/src/lib.rs b/chains/solana/contracts/programs/example-ccip-sender/src/lib.rs new file mode 100644 index 000000000..54c3876b6 --- /dev/null +++ b/chains/solana/contracts/programs/example-ccip-sender/src/lib.rs @@ -0,0 +1,325 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{Mint, TokenAccount}; + +declare_id!("CcipSender111111111111111111111111111111111"); + +pub const EXTERNAL_EXECUTION_CONFIG_SEED: &[u8] = b"external_execution_config"; +pub const APPROVED_SENDER_SEED: &[u8] = b"approved_ccip_sender"; +pub const TOKEN_ADMIN_SEED: &[u8] = b"receiver_token_admin"; + +/// This program an example of a CCIP Receiver Program. +/// Used to test CCIP Router execute. +#[program] +pub mod example_ccip_receiver { + use anchor_spl::token_2022::spl_token_2022::{self, instruction::transfer_checked}; + use solana_program::program::invoke_signed; + + use super::*; + + /// The initialization is responsibility of the External User, CCIP is not handling initialization of Accounts + pub fn initialize(ctx: Context, router: Pubkey) -> Result<()> { + ctx.accounts + .state + .init(ctx.accounts.authority.key(), router) + } + + pub fn ccip_send(_ctx: Context, _message: Any2SVMMessage) -> Result<()> { + // TODO + Ok(()) + } + + pub fn update_router(ctx: Context, new_router: Pubkey) -> Result<()> { + ctx.accounts + .state + .update_router(ctx.accounts.authority.key(), new_router) + } + + pub fn transfer_ownership(ctx: Context, proposed_owner: Pubkey) -> Result<()> { + ctx.accounts + .state + .transfer_ownership(ctx.accounts.authority.key(), proposed_owner) + } + + pub fn accept_ownership(ctx: Context) -> Result<()> { + ctx.accounts + .state + .accept_ownership(ctx.accounts.authority.key()) + } + + pub fn withdraw_tokens(ctx: Context, amount: u64, decimals: u8) -> Result<()> { + let mut ix = transfer_checked( + &spl_token_2022::ID, // use spl-token-2022 to compile instruction - change program later + &ctx.accounts.program_token_account.key(), + &ctx.accounts.mint.key(), + &ctx.accounts.to_token_account.key(), + &ctx.accounts.token_admin.key(), + &[], + amount, + decimals, + )?; + ix.program_id = ctx.accounts.token_program.key(); // set to user specified program + + let seeds = &[TOKEN_ADMIN_SEED, &[ctx.bumps.token_admin]]; + invoke_signed( + &ix, + &[ + ctx.accounts.program_token_account.to_account_info(), + ctx.accounts.mint.to_account_info(), + ctx.accounts.to_token_account.to_account_info(), + ctx.accounts.token_admin.to_account_info(), + ], + &[&seeds[..]], + )?; + Ok(()) + } +} + +const ANCHOR_DISCRIMINATOR: usize = 8; + +#[derive(Accounts, Debug)] +pub struct Initialize<'info> { + #[account( + init, + seeds = [b"state"], + bump, + payer = authority, + space = ANCHOR_DISCRIMINATOR + BaseState::INIT_SPACE, + )] + pub state: Account<'info, BaseState>, + #[account( + init, + seeds = [TOKEN_ADMIN_SEED], + bump, + payer = authority, + space = ANCHOR_DISCRIMINATOR, + )] + /// CHECK: CPI signer for tokens + pub token_admin: UncheckedAccount<'info>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts, Debug)] +#[instruction(message: Any2SVMMessage)] +pub struct CcipReceive<'info> { + // router CPI signer must be first + #[account( + constraint = state.is_router(authority.key()) @ CcipReceiverError::OnlyRouter, + )] + pub authority: Signer<'info>, + #[account( + seeds = [ + APPROVED_SENDER_SEED, + message.source_chain_selector.to_le_bytes().as_ref(), + &[message.sender.len() as u8], + &message.sender, + ], + bump, + )] + pub approved_sender: Account<'info, ApprovedSender>, // if PDA does not exist, the message sender and/or source chain are not approved + pub state: Account<'info, BaseState>, +} + +#[derive(Accounts, Debug)] +pub struct UpdateConfig<'info> { + #[account( + mut, + seeds = [b"state"], + bump, + )] + pub state: Account<'info, BaseState>, + #[account( + address = state.owner @ CcipReceiverError::OnlyOwner, + )] + pub authority: Signer<'info>, +} + +#[derive(Accounts, Debug)] +pub struct AcceptOwnership<'info> { + #[account( + mut, + seeds = [b"state"], + bump, + )] + pub state: Account<'info, BaseState>, + #[account( + address = state.proposed_owner @ CcipReceiverError::OnlyProposedOwner, + )] + pub authority: Signer<'info>, +} + +#[derive(Accounts, Debug)] +pub struct WithdrawTokens<'info> { + #[account( + mut, + seeds = [b"state"], + bump, + )] + pub state: Account<'info, BaseState>, + #[account( + mut, + token::mint = mint, + token::authority = token_admin, + token::token_program = token_program, + )] + pub program_token_account: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + token::mint = mint, + token::token_program = token_program, + )] + pub to_token_account: InterfaceAccount<'info, TokenAccount>, + pub mint: InterfaceAccount<'info, Mint>, + #[account(address = *mint.to_account_info().owner)] + /// CHECK: CPI to token program + pub token_program: AccountInfo<'info>, + #[account( + seeds = [TOKEN_ADMIN_SEED], + bump, + )] + /// CHECK: CPI signer for tokens + pub token_admin: UncheckedAccount<'info>, + #[account( + address = state.owner @ CcipReceiverError::OnlyOwner, + )] + pub authority: Signer<'info>, +} + +// BaseState contains the state for core safety checks that can be leveraged by the implementer +// Base state contains a limited size allow and deny list +// Both are included to handle the size limitations on solana +// If user wants to allow a small number of chains, consider using the allow list (disable deny list) +// If user wants to allow many chains, consider using the deny list (disable allow list) +#[account] +#[derive(InitSpace, Default, Debug)] +pub struct BaseState { + pub owner: Pubkey, + pub proposed_owner: Pubkey, + + pub router: Pubkey, +} + +impl BaseState { + pub fn init(&mut self, owner: Pubkey, router: Pubkey) -> Result<()> { + require_eq!(self.owner, Pubkey::default()); + self.owner = owner; + self.update_router(owner, router) + } + + pub fn transfer_ownership(&mut self, owner: Pubkey, proposed_owner: Pubkey) -> Result<()> { + require_eq!(self.owner, owner, CcipReceiverError::OnlyOwner); + self.proposed_owner = proposed_owner; + Ok(()) + } + + pub fn accept_ownership(&mut self, proposed_owner: Pubkey) -> Result<()> { + require_eq!( + self.proposed_owner, + proposed_owner, + CcipReceiverError::OnlyProposedOwner + ); + self.proposed_owner = Pubkey::default(); + self.owner = proposed_owner; + Ok(()) + } + + pub fn is_router(&self, caller: Pubkey) -> bool { + Pubkey::find_program_address(&[EXTERNAL_EXECUTION_CONFIG_SEED], &self.router).0 == caller + } + + pub fn update_router(&mut self, owner: Pubkey, router: Pubkey) -> Result<()> { + require_keys_neq!(router, Pubkey::default(), CcipReceiverError::InvalidRouter); + require_eq!(self.owner, owner, CcipReceiverError::OnlyOwner); + self.router = router; + Ok(()) + } +} + +#[account] +#[derive(InitSpace, Default, Debug)] +pub struct ApprovedSender {} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct Any2SVMMessage { + pub message_id: [u8; 32], + pub source_chain_selector: u64, + pub sender: Vec, + pub data: Vec, + pub token_amounts: Vec, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default)] +pub struct SVMTokenAmount { + pub token: Pubkey, + pub amount: u64, // solana local token amount +} + +#[error_code] +pub enum CcipReceiverError { + #[msg("Address is not router external execution PDA")] + OnlyRouter, + #[msg("Invalid router address")] + InvalidRouter, + #[msg("Invalid combination of chain and sender")] + InvalidChainAndSender, + #[msg("Address is not owner")] + OnlyOwner, + #[msg("Address is not proposed_owner")] + OnlyProposedOwner, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_state() -> BaseState { + BaseState { + owner: Pubkey::new_unique(), + ..BaseState::default() + } + } + + #[test] + fn ownership() { + let mut state = create_state(); + let next_owner = Pubkey::new_unique(); + + // only owner can propose + assert_eq!( + state + .transfer_ownership(Pubkey::new_unique(), Pubkey::new_unique()) + .unwrap_err(), + CcipReceiverError::OnlyOwner.into() + ); + state.transfer_ownership(state.owner, next_owner).unwrap(); + + // only proposed_owner can accept + assert_eq!( + state.accept_ownership(Pubkey::new_unique()).unwrap_err(), + CcipReceiverError::OnlyProposedOwner.into(), + ); + state.accept_ownership(next_owner).unwrap(); + } + + #[test] + fn router() { + let mut state = create_state(); + + assert_eq!( + state + .update_router(state.owner, Pubkey::default()) + .unwrap_err(), + CcipReceiverError::InvalidRouter.into(), + ); + assert_eq!( + state + .update_router(Pubkey::new_unique(), Pubkey::new_unique()) + .unwrap_err(), + CcipReceiverError::OnlyOwner.into(), + ); + state + .update_router(state.owner, Pubkey::new_unique()) + .unwrap(); + } +}