-
Notifications
You must be signed in to change notification settings - Fork 7
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][NONEVM-1246] Implement allowlist on onramp #520
Changes from all commits
3a5f3e3
8f697d5
e4d4172
1488282
6f46ba4
e81b98b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -114,6 +114,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, | ||
Comment on lines
+123
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we double check the product requirements for if it's unbounded, then we should allowlist based on a user specific PDA since there is a maximum account size that would bound the list There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checked by @agusaldasoro , we're OK to go with the boundary enforced by maximum account size. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We estimated a max size of 300k which is okay |
||
} | ||
|
||
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>() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't arrays have a base size of 4? I remember using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is captured by I find it a bit annoying that 'max_len' is so strangely named, because it's way more useful than I first thought: It should be called |
||
} | ||
} | ||
|
||
#[account] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -673,6 +673,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()}) | ||
|
@@ -702,6 +703,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()}) | ||
|
@@ -751,6 +753,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) | ||
|
@@ -2092,6 +2095,72 @@ 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add a test for sending with disabled allowlist as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's necessary: that's already captured by all the other tests, as the allowlist is disabled by default. |
||
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}, | ||
ExtraArgs: emptyEVMExtraArgsV2, | ||
} | ||
|
||
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 | ||
|
@@ -2851,6 +2920,83 @@ 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}, | ||
ExtraArgs: emptyEVMExtraArgsV2, | ||
} | ||
|
||
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If using false, then we need to ensure zeroing the bytes when decreasing the size of the allowed senders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. Do we? I'm not sure.
The logic is pretty simple and self-contained, so I'm not sure what damage leaving those bytes "dirty" could cause. If we downsize and upsize afterwards, all code paths lead to overwriting these bytes again, so I can't see how we'd end up with garbage in an allocated structure.
I might be missing some solana risks I'm not aware of here though, so if it's safer to make this
true
, I'm happy to.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the docs that I read recommend using realloc zero to true, or in this doc they recommend using an ALT table instead
https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security?utm_source=chatgpt.com#account-data-reallocation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This module talks about issues when init, but they may be valid for resize as well? https://solana.com/developers/courses/program-security/reinitialization-attacks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@agusaldasoro the guide you linked (and the official documentation) bot specify that
realloc::zero = true
is only necessary in the cases where a single instruction can downsize and then upsize in one call. This is impossible in our case as we only reallocate once on entry. This means the bytes are automatically zeroed, and usingrealloc::zero = true
would be wasted cycles.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh, okay! that's clear now. Can you please add a comment in code staying that so we don't forget about that in the future? thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good! :)