Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

solana: example ccip sender #539

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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();
}
}