Skip to content

Commit

Permalink
Implement allowlist on onramp
Browse files Browse the repository at this point in the history
  • Loading branch information
PabloMansanet committed Jan 29, 2025
1 parent 771fb99 commit a99f0b1
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 13 deletions.
35 changes: 30 additions & 5 deletions chains/solana/contracts/programs/ccip-router/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use solana_program::sysvar::instructions;
use crate::program::CcipRouter;
use crate::state::{CommitReport, Config, Nonce};
use crate::{
BillingTokenConfig, BillingTokenConfigWrapper, CcipRouterError, DestChain,
BillingTokenConfig, BillingTokenConfigWrapper, CcipRouterError, DestChain, DestChainConfig,
ExecutionReportSingleChain, ExternalExecutionConfig, GlobalState, SVM2AnyMessage, SourceChain,
};

Expand Down Expand Up @@ -254,11 +254,10 @@ pub struct AcceptOwnership<'info> {
}

#[derive(Accounts)]
#[instruction(new_chain_selector: u64)]
#[instruction(new_chain_selector: u64, dest_chain_config: DestChainConfig)]
pub struct AddChainSelector<'info> {
/// Adding a chain selector implies initializing the state for a new chain,
/// hence the need to initialize two accounts.
#[account(
init,
seeds = [seed::SOURCE_CHAIN_STATE, new_chain_selector.to_le_bytes().as_ref()],
Expand All @@ -273,7 +272,7 @@ pub struct AddChainSelector<'info> {
seeds = [seed::DEST_CHAIN_STATE, new_chain_selector.to_le_bytes().as_ref()],
bump,
payer = authority,
space = ANCHOR_DISCRIMINATOR + DestChain::INIT_SPACE,
space = ANCHOR_DISCRIMINATOR + DestChain::INIT_SPACE + dest_chain_config.dynamic_space(),
)]
pub dest_chain_state: Account<'info, DestChain>,

Expand Down Expand Up @@ -312,8 +311,34 @@ pub struct UpdateSourceChainSelectorConfig<'info> {
}

#[derive(Accounts)]
#[instruction(new_chain_selector: u64)]
#[instruction(new_chain_selector: u64, dest_chain_config: DestChainConfig)]
pub struct UpdateDestChainSelectorConfig<'info> {
#[account(
mut,
seeds = [seed::DEST_CHAIN_STATE, new_chain_selector.to_le_bytes().as_ref()],
bump,
constraint = valid_version(dest_chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs,
realloc = ANCHOR_DISCRIMINATOR + DestChain::INIT_SPACE + dest_chain_config.dynamic_space(),
realloc::payer = authority,
realloc::zero = false
)]
pub dest_chain_state: Account<'info, DestChain>,

#[account(
seeds = [seed::CONFIG],
bump,
constraint = valid_version(config.load()?.version, MAX_CONFIG_V) @ CcipRouterError::InvalidInputs,
)]
pub config: AccountLoader<'info, Config>,

#[account(mut, address = config.load()?.owner @ CcipRouterError::Unauthorized)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
#[instruction(new_chain_selector: u64)]
pub struct DisableDestChainSelectorConfig<'info> {
#[account(
mut,
seeds = [seed::DEST_CHAIN_STATE, new_chain_selector.to_le_bytes().as_ref()],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anchor_lang::prelude::*;
use anchor_spl::token_interface;

use crate::seed;
use crate::{seed, DisableDestChainSelectorConfig};
use crate::{
AcceptOwnership, AddBillingTokenConfig, AddChainSelector, BillingTokenConfig, CcipRouterError,
DestChainAdded, DestChainConfig, DestChainConfigUpdated, DestChainState, FeeTokenAdded,
Expand Down Expand Up @@ -107,7 +107,7 @@ pub fn disable_source_chain_selector(
}

pub fn disable_dest_chain_selector(
ctx: Context<UpdateDestChainSelectorConfig>,
ctx: Context<DisableDestChainSelectorConfig>,
dest_chain_selector: u64,
) -> Result<()> {
let chain_state = &mut ctx.accounts.dest_chain_state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ pub mod ramps {
gas_price_staleness_threshold: 90000,
enforce_out_of_order: false,
chain_family_selector: CHAIN_FAMILY_SELECTOR_EVM.to_be_bytes(),
allowed_senders: vec![],
allow_list_enabled: false,
},
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,15 @@ pub fn ccip_send<'info>(
// The Config Account stores the default values for the Router, the SVM Chain Selector, the Default Gas Limit and the Default Allow Out Of Order Execution and Admin Ownership
let config = ctx.accounts.config.load()?;

let sender = ctx.accounts.authority.key.to_owned();
let dest_chain = &mut ctx.accounts.dest_chain_state;

require!(
!dest_chain.config.allow_list_enabled
|| dest_chain.config.allowed_senders.contains(&sender),
CcipRouterError::SenderNotAllowed
);

let mut accounts_per_sent_token: Vec<TokenAccounts> = vec![];

for (i, token_amount) in message.token_amounts.iter().enumerate() {
Expand Down Expand Up @@ -163,7 +170,6 @@ pub fn ccip_send<'info>(
);
dest_chain.state.sequence_number = overflow_add.unwrap();

let sender = ctx.accounts.authority.key.to_owned();
let receiver = message.receiver.clone();
let source_chain_selector = config.svm_chain_selector;
let extra_args = extra_args_or_default(config, message.extra_args);
Expand Down
4 changes: 3 additions & 1 deletion chains/solana/contracts/programs/ccip-router/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ pub mod ccip_router {
/// * `ctx` - The context containing the accounts required for disabling the chain selector.
/// * `dest_chain_selector` - The destination chain selector to be disabled.
pub fn disable_dest_chain_selector(
ctx: Context<UpdateDestChainSelectorConfig>,
ctx: Context<DisableDestChainSelectorConfig>,
dest_chain_selector: u64,
) -> Result<()> {
v1::admin::disable_dest_chain_selector(ctx, dest_chain_selector)
Expand Down Expand Up @@ -691,4 +691,6 @@ pub enum CcipRouterError {
ExtraArgOutOfOrderExecutionMustBeTrue,
#[msg("Invalid writability bitmap")]
InvalidWritabilityBitmap,
#[msg("Sender not allowed for that destination chain")]
SenderNotAllowed,
}
19 changes: 19 additions & 0 deletions chains/solana/contracts/programs/ccip-router/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,25 @@ pub struct DestChainConfig {
pub gas_price_staleness_threshold: u32, // The amount of time a gas price can be stale before it is considered invalid (0 means disabled)
pub enforce_out_of_order: bool, // Whether to enforce the allowOutOfOrderExecution extraArg value to be true.
pub chain_family_selector: [u8; 4], // Selector that identifies the destination chain's family. Used to determine the correct validations to perform for the dest chain.

// list of senders authorized to send messages to this destination chain.
// Note: The attribute name `max_len` is slightly misleading: it is not in any
// way limiting the actual length of the vector during initialization; it just
// helps the InitSpace derive macro work out the initial space. We can leave it at
// zero and calculate the actual length in the instruction context.
#[max_len(0)]
pub allowed_senders: Vec<Pubkey>,
pub allow_list_enabled: bool,
}

impl DestChainConfig {
pub fn space(&self) -> usize {
Self::INIT_SPACE + self.dynamic_space()
}

pub fn dynamic_space(&self) -> usize {
self.allowed_senders.len() * std::mem::size_of::<Pubkey>()
}
}

#[account]
Expand Down
18 changes: 18 additions & 0 deletions chains/solana/contracts/target/idl/ccip_router.json
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@
"name": "authority",
"isMut": true,
"isSigner": true
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
Expand Down Expand Up @@ -2560,6 +2565,16 @@
4
]
}
},
{
"name": "allowedSenders",
"type": {
"vec": "publicKey"
}
},
{
"name": "allowListEnabled",
"type": "bool"
}
]
}
Expand Down Expand Up @@ -2835,6 +2850,9 @@
},
{
"name": "InvalidWritabilityBitmap"
},
{
"name": "SenderNotAllowed"
}
]
}
Expand Down
144 changes: 144 additions & 0 deletions chains/solana/contracts/tests/ccip/ccip_router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ func TestCCIPRouter(t *testing.T) {
destChainStatePDA,
config.RouterConfigPDA,
admin.PublicKey(),
solana.SystemProgramID,
).ValidateAndBuild()
require.NoError(t, err)
result := testutils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment, []string{"Error Code: " + ccip_router.InvalidInputs_CcipRouterError.String()})
Expand Down Expand Up @@ -752,6 +753,7 @@ func TestCCIPRouter(t *testing.T) {
config.EvmDestChainStatePDA,
config.RouterConfigPDA,
user.PublicKey(), // unauthorized
solana.SystemProgramID,
).ValidateAndBuild()
require.NoError(t, err)
result := testutils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: " + ccip_router.Unauthorized_CcipRouterError.String()})
Expand Down Expand Up @@ -801,6 +803,7 @@ func TestCCIPRouter(t *testing.T) {
config.EvmDestChainStatePDA,
config.RouterConfigPDA,
admin.PublicKey(),
solana.SystemProgramID,
).ValidateAndBuild()
require.NoError(t, err)
result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment)
Expand Down Expand Up @@ -2138,6 +2141,71 @@ func TestCCIPRouter(t *testing.T) {
require.NotNil(t, result)
})

t.Run("When sending with an empty but enabled allowlist, it fails", func(t *testing.T) {
var initialDestChain ccip_router.DestChain
err := common.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &initialDestChain)
require.NoError(t, err, "failed to get account info")
modifiedDestChain := initialDestChain
modifiedDestChain.Config.AllowListEnabled = true

updateDestChainIx, err := ccip_router.NewUpdateDestChainConfigInstruction(
config.EvmChainSelector,
modifiedDestChain.Config,
config.EvmDestChainStatePDA,
config.RouterConfigPDA,
anotherAdmin.PublicKey(),
solana.SystemProgramID,
).ValidateAndBuild()
require.NoError(t, err)
result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{updateDestChainIx}, anotherAdmin, config.DefaultCommitment)
require.NotNil(t, result)

destinationChainSelector := config.EvmChainSelector
destinationChainStatePDA := config.EvmDestChainStatePDA
message := ccip_router.SVM2AnyMessage{
FeeToken: wsol.mint,
Receiver: validReceiverAddress[:],
Data: []byte{4, 5, 6},
}

raw := ccip_router.NewCcipSendInstruction(
destinationChainSelector,
message,
[]byte{},
config.RouterConfigPDA,
destinationChainStatePDA,
nonceEvmPDA,
user.PublicKey(),
solana.SystemProgramID,
wsol.program,
wsol.mint,
wsol.billingConfigPDA,
token2022.billingConfigPDA,
wsol.userATA,
wsol.billingATA,
config.BillingSignerPDA,
config.ExternalTokenPoolsSignerPDA,
)
raw.GetFeeTokenUserAssociatedAccountAccount().WRITE()
instruction, err := raw.ValidateAndBuild()
require.NoError(t, err)
result = testutils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: SenderNotAllowed"})
require.NotNil(t, result)

// We now restore the config to keep the test state-neutral
updateDestChainIx, err = ccip_router.NewUpdateDestChainConfigInstruction(
config.EvmChainSelector,
initialDestChain.Config,
config.EvmDestChainStatePDA,
config.RouterConfigPDA,
anotherAdmin.PublicKey(),
solana.SystemProgramID,
).ValidateAndBuild()
require.NoError(t, err)
result = testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{updateDestChainIx}, anotherAdmin, config.DefaultCommitment)
require.NotNil(t, result)
})

t.Run("When sending a Valid CCIP Message Emits CCIPMessageSent", func(t *testing.T) {
destinationChainSelector := config.EvmChainSelector
destinationChainStatePDA := config.EvmDestChainStatePDA
Expand Down Expand Up @@ -2960,6 +3028,82 @@ func TestCCIPRouter(t *testing.T) {
})
})

t.Run("When sending with an enabled allowlist including the sender, it succeeds", func(t *testing.T) {
var initialDestChain ccip_router.DestChain
err := common.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &initialDestChain)
require.NoError(t, err, "failed to get account info")
modifiedDestChain := initialDestChain
modifiedDestChain.Config.AllowListEnabled = true
modifiedDestChain.Config.AllowedSenders = []solana.PublicKey{
user.PublicKey(),
anotherUser.PublicKey(),
}

updateDestChainIx, err := ccip_router.NewUpdateDestChainConfigInstruction(
config.EvmChainSelector,
modifiedDestChain.Config,
config.EvmDestChainStatePDA,
config.RouterConfigPDA,
anotherAdmin.PublicKey(),
solana.SystemProgramID,
).ValidateAndBuild()
require.NoError(t, err)
result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{updateDestChainIx}, anotherAdmin, config.DefaultCommitment)
require.NotNil(t, result)

var parsedDestChain ccip_router.DestChain
err = common.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &parsedDestChain)
require.NoError(t, err, "failed to get account info")

// This proves we're able to update the config with a dynamically sized element
require.Equal(t, parsedDestChain.Config.AllowedSenders, modifiedDestChain.Config.AllowedSenders)

destinationChainSelector := config.EvmChainSelector
destinationChainStatePDA := config.EvmDestChainStatePDA
message := ccip_router.SVM2AnyMessage{
FeeToken: wsol.mint,
Receiver: validReceiverAddress[:],
Data: []byte{4, 5, 6},
}

raw := ccip_router.NewCcipSendInstruction(
destinationChainSelector,
message,
[]byte{},
config.RouterConfigPDA,
destinationChainStatePDA,
nonceEvmPDA,
user.PublicKey(),
solana.SystemProgramID,
wsol.program,
wsol.mint,
wsol.billingConfigPDA,
token2022.billingConfigPDA,
wsol.userATA,
wsol.billingATA,
config.BillingSignerPDA,
config.ExternalTokenPoolsSignerPDA,
)
raw.GetFeeTokenUserAssociatedAccountAccount().WRITE()
instruction, err := raw.ValidateAndBuild()
require.NoError(t, err)
result = testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment)
require.NotNil(t, result)

// We now restore the config to keep the test state-neutral
updateDestChainIx, err = ccip_router.NewUpdateDestChainConfigInstruction(
config.EvmChainSelector,
initialDestChain.Config,
config.EvmDestChainStatePDA,
config.RouterConfigPDA,
anotherAdmin.PublicKey(),
solana.SystemProgramID,
).ValidateAndBuild()
require.NoError(t, err)
result = testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{updateDestChainIx}, anotherAdmin, config.DefaultCommitment)
require.NotNil(t, result)
})

t.Run("token pool accounts: validation", func(t *testing.T) {
t.Parallel()
// base transaction
Expand Down
Loading

0 comments on commit a99f0b1

Please sign in to comment.