-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
358 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
chains/solana/contracts/programs/example-ccip-sender/Cargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
2 changes: 2 additions & 0 deletions
2
chains/solana/contracts/programs/example-ccip-sender/Xargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
325
chains/solana/contracts/programs/example-ccip-sender/src/lib.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |