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] Allowlist on token pool #564

Draft
wants to merge 8 commits 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
2 changes: 2 additions & 0 deletions chains/solana/contracts/Cargo.lock

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

21 changes: 21 additions & 0 deletions chains/solana/contracts/programs/ccip-offramp/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ pub struct CommitReportContext<'info> {
// [...chainConfig accounts] fee quoter accounts used to store gas prices
}

const ALLOWED_OFFRAMP: &[u8] = b"allowed_offramp";

#[derive(Accounts)]
#[instruction(raw_report: Vec<u8>)]
pub struct ExecuteReportContext<'info> {
Expand Down Expand Up @@ -374,6 +376,25 @@ pub struct ExecuteReportContext<'info> {
)]
pub commit_report: Account<'info, CommitReport>,

pub offramp: Program<'info, CcipOfframp>,

/// CHECK PDA of the router program verifying the signer is an allowed offramp.
/// If PDA does not exist, the router doesn't allow this offramp
#[account(
constraint = {
let (pda, _) = Pubkey::find_program_address(
&[
ALLOWED_OFFRAMP,
source_chain.chain_selector.to_le_bytes().as_ref(),
offramp.key().as_ref(),
],
&reference_addresses.router,
);
allowed_offramp.key() == pda && allowed_offramp.owner.key() == reference_addresses.router
} @ CcipOfframpError::InvalidInputs
)]
pub allowed_offramp: UncheckedAccount<'info>,

/// CHECK: Using this to sign
#[account(seeds = [seed::EXTERNAL_EXECUTION_CONFIG], bump)]
pub external_execution_config: Account<'info, ExternalExecutionConfig>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,18 @@ fn internal_execute<'info>(
source_pool_data: token_amount.extra_data.clone(),
offchain_token_data: execution_report.offchain_token_data[i].clone(),
};
let mut acc_infos = router_token_pool_signer.to_account_infos();
acc_infos.extend_from_slice(&[
let mut acc_infos = vec![
router_token_pool_signer.to_account_info(),
ctx.accounts.offramp.to_account_info(),
ctx.accounts.allowed_offramp.to_account_info(),
accs.pool_config.to_account_info(),
accs.token_program.to_account_info(),
accs.mint.to_account_info(),
accs.pool_signer.to_account_info(),
accs.pool_token_account.to_account_info(),
accs.pool_chain_config.to_account_info(),
accs.user_token_account.to_account_info(),
]);
];
acc_infos.extend_from_slice(accs.remaining_accounts);
let return_data = interact_with_pool(
accs.pool_program.key(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ default = []

[dependencies]
anchor-lang = "0.29.0"
anchor-spl = "0.29.0"
solana-program = "1.17.25" # pin solana to 1.17
example_ccip_receiver = { version = "0.1.0-dev", path = "../example-ccip-receiver", features = ["no-entrypoint"] }
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
* Used to test CCIP Router execute and check that it fails
*/
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{Mint, TokenAccount};
use example_ccip_receiver::Any2SVMMessage;
use program::TestCcipInvalidReceiver;

declare_id!("9Vjda3WU2gsJgE4VdU6QuDw8rfHLyigfFyWs3XDPNUn8");

#[program]
pub mod test_ccip_invalid_receiver {
use anchor_lang::solana_program::instruction::Instruction;
use anchor_lang::solana_program::program::{get_return_data, invoke_signed};

use super::*;

pub fn ccip_receive(ctx: Context<Initialize>, _message: Any2SVMMessage) -> Result<()> {
Expand All @@ -19,6 +24,51 @@ pub mod test_ccip_invalid_receiver {

Ok(())
}

// This is just a dumb proxy towards the test_pool program, but signing the call with a PDA that mimics
// what the offramp does
pub fn pool_proxy_release_or_mint<'info>(
ctx: Context<'_, '_, 'info, 'info, PoolProxyReleaseOrMint<'info>>,
release_or_mint: ReleaseOrMintInV1,
) -> Result<Vec<u8>> {
let mut acc_infos = vec![
ctx.accounts.cpi_signer.to_account_info(),
ctx.accounts.offramp_program.to_account_info(),
ctx.accounts.allowed_offramp.to_account_info(),
ctx.accounts.config.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.pool_signer.to_account_info(),
ctx.accounts.pool_token_account.to_account_info(),
ctx.accounts.chain_config.to_account_info(),
ctx.accounts.receiver_token_account.to_account_info(),
];

acc_infos.extend_from_slice(ctx.remaining_accounts);

let acc_metas: Vec<AccountMeta> = acc_infos
.iter()
.flat_map(|acc_info| {
// Check signer from PDA External Execution config
let is_signer = acc_info.key() == ctx.accounts.cpi_signer.key();
acc_info.to_account_metas(Some(is_signer))
})
.collect();

let ix = Instruction {
program_id: ctx.accounts.test_pool.key(),
accounts: acc_metas,
data: release_or_mint.to_tx_data(),
};

let seeds: &[&[u8]] = &[b"external_token_pools_signer", &[ctx.bumps.cpi_signer]];

invoke_signed(&ix, &acc_infos, &[seeds])?;

let (_, data) = get_return_data().unwrap();

Ok(data)
}
}

const ANCHOR_DISCRIMINATOR: usize = 8;
Expand Down Expand Up @@ -46,3 +96,76 @@ pub struct Initialize<'info> {
pub struct Counter {
pub value: u8,
}

#[derive(Accounts)]
#[instruction(release_or_mint: ReleaseOrMintInV1)]
pub struct PoolProxyReleaseOrMint<'info> {
/// CHECK
pub test_pool: UncheckedAccount<'info>,

/// CHECK
#[account(
seeds = [b"external_token_pools_signer"],
bump,
)]
pub cpi_signer: UncheckedAccount<'info>,

///////////////////////////////////
// Accounts required by Pool CPI //
///////////////////////////////////
pub offramp_program: Program<'info, TestCcipInvalidReceiver>, // this receiver acts as "dumb" offramp here

/// CHECK
pub allowed_offramp: UncheckedAccount<'info>,

/// CHECK
#[account(mut)]
pub config: UncheckedAccount<'info>,

/// CHECK
pub token_program: AccountInfo<'info>,

#[account(mut)]
pub mint: InterfaceAccount<'info, Mint>,

/// CHECK
pub pool_signer: UncheckedAccount<'info>,

#[account(mut)]
pub pool_token_account: InterfaceAccount<'info, TokenAccount>,

/// CHECK
#[account(mut)]
pub chain_config: UncheckedAccount<'info>,

/// CHECK
#[account(mut)]
pub receiver_token_account: UncheckedAccount<'info>,
}

#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
pub struct ReleaseOrMintInV1 {
original_sender: Vec<u8>, // The original sender of the tx on the source chain
remote_chain_selector: u64, // ─╮ The chain ID of the source chain
receiver: Pubkey, // ───────────╯ The recipient of the tokens on the destination chain.
amount: [u8; 32], // u256, incoming cross-chain amount - The amount of tokens to release or mint, denominated in the source token's decimals
local_token: Pubkey, // The address on this chain of the token to release or mint
/// @dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the
/// expected pool address for the given remoteChainSelector.
source_pool_address: Vec<u8>, // The address of the source pool, abi encoded in the case of EVM chains
source_pool_data: Vec<u8>, // The data received from the source pool to process the release or mint
/// @dev WARNING: offchainTokenData is untrusted data.
offchain_token_data: Vec<u8>, // The offchain data to process the release or mint
}

pub const TOKENPOOL_RELEASE_OR_MINT_DISCRIMINATOR: [u8; 8] =
[0x5c, 0x64, 0x96, 0xc6, 0xfc, 0x3f, 0xa4, 0xe4]; // release_or_mint_tokens

impl ReleaseOrMintInV1 {
pub fn to_tx_data(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&TOKENPOOL_RELEASE_OR_MINT_DISCRIMINATOR);
data.extend_from_slice(&self.try_to_vec().unwrap());
data
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use anchor_spl::{token_interface::Mint, token_interface::TokenAccount};
use example_ccip_receiver::{
Any2SVMMessage, BaseState, CcipReceiverError, EXTERNAL_EXECUTION_CONFIG_SEED,
};

use solana_program::pubkey;
declare_id!("CtEVnHsQzhTNWav8skikiV2oF6Xx7r7uGGa8eCDQtTjH");

Expand All @@ -12,7 +13,8 @@ declare_id!("CtEVnHsQzhTNWav8skikiV2oF6Xx7r7uGGa8eCDQtTjH");
pub mod test_ccip_receiver {
const CCIP_ROUTER: Pubkey = pubkey!("offRPDpDxT5MGFNmMh99QKTZfPWTkqYUrStEriAS1H5");

use solana_program::{instruction::Instruction, program::invoke_signed};
use solana_program::instruction::Instruction;
use solana_program::program::invoke_signed;

use super::*;

Expand Down
34 changes: 32 additions & 2 deletions chains/solana/contracts/programs/token-pool/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ const ANCHOR_DISCRIMINATOR: usize = 8; // 8-byte anchor discriminator length
const CCIP_TOKENPOOL_CONFIG: &[u8] = b"ccip_tokenpool_config";
pub const CCIP_TOKENPOOL_SIGNER: &[u8] = b"ccip_tokenpool_signer";
pub const CCIP_TOKENPOOL_CHAINCONFIG: &[u8] = b"ccip_tokenpool_chainconfig";
pub const EXTERNAL_TOKENPOOL_SIGNER: &[u8] = b"external_token_pools_signer";
pub const RELEASE_MINT: [u8; 8] = [0x14, 0x94, 0x71, 0xc6, 0xe5, 0xaa, 0x47, 0x30];
pub const LOCK_BURN: [u8; 8] = [0xc8, 0x0e, 0x32, 0x09, 0x2c, 0x5b, 0x79, 0x25];
pub const ALLOWED_OFFRAMP: &[u8] = b"allowed_offramp";

#[derive(Accounts)]
pub struct InitializeTokenPool<'info> {
Expand Down Expand Up @@ -71,9 +73,31 @@ pub struct AcceptOwnership<'info> {
#[instruction(release_or_mint: ReleaseOrMintInV1)]
pub struct TokenOfframp<'info> {
// CCIP accounts ------------------------
#[account(address = config.ramp_authority @ CcipTokenPoolError::InvalidPoolCaller)]
#[account(
seeds = [EXTERNAL_TOKENPOOL_SIGNER],
bump,
seeds::program = offramp_program.key(),
)]
pub authority: Signer<'info>,

/// CHECK offramp program: exists only to derive the allowed offramp PDA
/// and the authority PDA.
pub offramp_program: UncheckedAccount<'info>,

/// CHECK PDA of the router program verifying the signer is an allowed offramp.
/// If PDA does not exist, the router doesn't allow this offramp
#[account(
owner = config.ccip_router @ CcipTokenPoolError::InvalidPoolCaller, // this guarantees that it was initialized
seeds = [
ALLOWED_OFFRAMP,
release_or_mint.remote_chain_selector.to_le_bytes().as_ref(),
offramp_program.key().as_ref()
],
bump,
seeds::program = config.ccip_router,
)]
pub allowed_offramp: UncheckedAccount<'info>,

// Token pool accounts ------------------
// consistent set + token pool program
#[account(
Expand Down Expand Up @@ -341,7 +365,13 @@ pub struct RemoteChainRemoved {
}

#[event]
pub struct RouterUpdated {
pub struct RampAuthorityUpdated {
pub old_authority: Pubkey,
pub new_authority: Pubkey,
}

#[event]
pub struct RouterUpdated {
pub old_router: Pubkey,
pub new_router: Pubkey,
}
28 changes: 24 additions & 4 deletions chains/solana/contracts/programs/token-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod token_pool {
ctx: Context<InitializeTokenPool>,
pool_type: PoolType,
ramp_authority: Pubkey,
ccip_router: Pubkey,
) -> Result<()> {
let token_info = ctx.accounts.mint.to_account_info();

Expand All @@ -45,6 +46,7 @@ pub mod token_pool {
);
config.owner = ctx.accounts.authority.key();
config.ramp_authority = ramp_authority;
config.ccip_router = ccip_router;

Ok(())
}
Expand All @@ -67,8 +69,7 @@ pub mod token_pool {
Ok(())
}

// set_ramp_authority changes the expected signer for mint/release + burn/lock method calls
// this is used to update the router address
// set_ramp_authority changes the expected signer for burn/lock method calls
pub fn set_ramp_authority(ctx: Context<SetConfig>, new_authority: Pubkey) -> Result<()> {
require!(
new_authority != Pubkey::zeroed(),
Expand All @@ -77,13 +78,30 @@ pub mod token_pool {

let old_authority = ctx.accounts.config.ramp_authority;
ctx.accounts.config.ramp_authority = new_authority;
emit!(RouterUpdated {
emit!(RampAuthorityUpdated {
old_authority,
new_authority
});
Ok(())
}

// set_router changes the router program ID. This is used to derive the list
// of valid mint/release callers (offramps).
pub fn set_router(ctx: Context<SetConfig>, new_router: Pubkey) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this method be cyclic in some way? Is that a concern?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean exactly about cyclic in this case 🤔
This method may only be invoked by the pool owner, not the router. Then, the router address here is used just to later check if the calling offramp is a valid one or not.

I've checked a bit and we already have another field in the pool though called ramp_authority that has the onramp address... And that's always the same as the router address, so we could just reuse it (even though in EVM they are separate concepts, here they are pretty much the same thing....)

require!(
new_router != Pubkey::zeroed(),
CcipTokenPoolError::InvalidInputs
);

let old_router = ctx.accounts.config.ccip_router;
ctx.accounts.config.ccip_router = new_router;
emit!(RouterUpdated {
old_router,
new_router
});
Ok(())
}

// initialize remote config (with no remote pools as it must be zero sized)
pub fn init_chain_remote_config(
ctx: Context<InitializeChainConfig>,
Expand Down Expand Up @@ -507,7 +525,9 @@ pub struct Config {
// ownership
pub owner: Pubkey,
pub proposed_owner: Pubkey,
ramp_authority: Pubkey, // signer for CCIP calls

ramp_authority: Pubkey,
ccip_router: Pubkey,
}

#[account]
Expand Down
Loading