Skip to content

Commit

Permalink
example-ccip-sender framework
Browse files Browse the repository at this point in the history
  • Loading branch information
aalu1418 committed Jan 31, 2025
1 parent 76ce883 commit ef7cdba
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 2 deletions.
1 change: 1 addition & 0 deletions chains/solana/contracts/Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion chains/solana/contracts/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions chains/solana/contracts/programs/example-ccip-sender/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
325 changes: 325 additions & 0 deletions chains/solana/contracts/programs/example-ccip-sender/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Initialize>, router: Pubkey) -> Result<()> {
ctx.accounts
.state
.init(ctx.accounts.authority.key(), router)
}

pub fn ccip_send(_ctx: Context<CcipReceive>, _message: Any2SVMMessage) -> Result<()> {
// TODO
Ok(())
}

pub fn update_router(ctx: Context<UpdateConfig>, new_router: Pubkey) -> Result<()> {
ctx.accounts
.state
.update_router(ctx.accounts.authority.key(), new_router)
}

pub fn transfer_ownership(ctx: Context<UpdateConfig>, proposed_owner: Pubkey) -> Result<()> {
ctx.accounts
.state
.transfer_ownership(ctx.accounts.authority.key(), proposed_owner)
}

pub fn accept_ownership(ctx: Context<AcceptOwnership>) -> Result<()> {
ctx.accounts
.state
.accept_ownership(ctx.accounts.authority.key())
}

pub fn withdraw_tokens(ctx: Context<WithdrawTokens>, 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<u8>,
pub data: Vec<u8>,
pub token_amounts: Vec<SVMTokenAmount>,
}

#[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();
}
}

0 comments on commit ef7cdba

Please sign in to comment.