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

fix(svm): across plus to solana #747

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
5 changes: 4 additions & 1 deletion Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ resolution = true
skip-lint = false

[programs.localnet]
multicall_handler = "6zbEkDZGuHqGiACGWc2Xd5DY4m52qwXjmthzWtnoCTyG"
svm_spoke = "Fdedr2RqfufUiE1sbVEfpSQ3NADJqxrvu1zojWpQJj4q"
test = "84j1xFuoz2xynhesB8hxC5N1zaWPr4MW1DD2gVm9PUs4"

[programs.devnet]
multicall_handler = "6zbEkDZGuHqGiACGWc2Xd5DY4m52qwXjmthzWtnoCTyG"
svm_spoke = "Fdedr2RqfufUiE1sbVEfpSQ3NADJqxrvu1zojWpQJj4q"

[registry]
Expand All @@ -33,9 +35,10 @@ closeRelayerPdas = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/closeRelay
closeDataWorkerLookUpTables = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/closeDataWorkerLookUpTables.ts"
remotePauseDeposits = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/remotePauseDeposits.ts"
generateExternalTypes = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/generateExternalTypes.ts"
fakeFillWithRandomDistribution = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/fakeFillWithRandomDistribution.ts"

[test.validator]
url = "https://api.devnet.solana.com"
url = "https://api.mainnet-beta.solana.com"

### Forked Circle Message Transmitter Program
[[test.validator.clone]]
Expand Down
8 changes: 8 additions & 0 deletions 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 programs/multicall-handler/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "multicall-handler"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "multicall_handler"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]
test = []

[dependencies]
anchor-lang = "0.30.1"
2 changes: 2 additions & 0 deletions programs/multicall-handler/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
73 changes: 73 additions & 0 deletions programs/multicall-handler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use anchor_lang::{
prelude::*,
solana_program::{
instruction::Instruction,
program::{invoke, invoke_signed},
},
};

declare_id!("6zbEkDZGuHqGiACGWc2Xd5DY4m52qwXjmthzWtnoCTyG");

#[program]
pub mod multicall_handler {
use super::*;

// Handler to receive AcrossV3 message formatted as serialized message compiled instructions. When deserialized,
// these are matched with the passed accounts and executed as CPIs.
pub fn handle_v3_across_message(ctx: Context<HandleV3AcrossMessage>, message: Vec<u8>) -> Result<()> {
// Some instructions might require being signed by handler PDA.
let (handler_signer, bump) = Pubkey::find_program_address(&[b"handler_signer"], &crate::ID);
let mut use_handler_signer = false;

let compiled_ixs: Vec<CompiledIx> = AnchorDeserialize::deserialize(&mut &message[..])?;

for compiled_ix in compiled_ixs {
let mut accounts = Vec::with_capacity(compiled_ix.account_key_indexes.len());
let mut account_infos = Vec::with_capacity(compiled_ix.account_key_indexes.len());

let target_program = ctx
.remaining_accounts
.get(compiled_ix.program_id_index as usize)
.ok_or(ErrorCode::AccountNotEnoughKeys)?;

// Resolve CPI accounts from indexed references to the remaining accounts.
for index in compiled_ix.account_key_indexes {
let account_info = ctx
.remaining_accounts
.get(index as usize)
.ok_or(ErrorCode::AccountNotEnoughKeys)?;
let is_handler_signer = account_info.key() == handler_signer;
use_handler_signer |= is_handler_signer;

match account_info.is_writable {
true => accounts.push(AccountMeta::new(account_info.key(), is_handler_signer)),
false => accounts.push(AccountMeta::new_readonly(account_info.key(), is_handler_signer)),
}
account_infos.push(account_info.to_owned());
}

let cpi_instruction = Instruction {
program_id: target_program.key(),
accounts,
data: compiled_ix.data,
};

match use_handler_signer {
true => invoke_signed(&cpi_instruction, &account_infos, &[&[b"handler_signer", &[bump]]])?,
false => invoke(&cpi_instruction, &account_infos)?,
}
}

Ok(())
}
}

#[derive(AnchorDeserialize)]
pub struct CompiledIx {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This resembles compiled instructions within a transaction message, except we don't use the compact-u16 encoding for the sake of code simplicity.

pub program_id_index: u8,
pub account_key_indexes: Vec<u8>,
pub data: Vec<u8>,
}

#[derive(Accounts)]
pub struct HandleV3AcrossMessage {}
3 changes: 2 additions & 1 deletion programs/svm-spoke/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ test = []
[dependencies]
anchor-lang = { version = "0.30.1", features = ["init-if-needed","event-cpi"]}
anchor-spl = "0.30.1"
solana-program = "=2.0.3"
solana-program = "=2.0.3"
multicall-handler = { path = "../multicall-handler" }
21 changes: 21 additions & 0 deletions programs/svm-spoke/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,24 @@ pub enum CallDataError {
#[msg("Unsupported solidity selector")]
UnsupportedSelector,
}

// Errors to handle Across+ message calls.
#[error_code]
pub enum AcrossPlusError {
#[msg("Message did not deserialize")]
MessageDidNotDeserialize,
#[msg("Invalid handle message key length")]
InvalidMessageKeyLength,
#[msg("Invalid handle message read-only key length")]
InvalidReadOnlyKeyLength,
#[msg("Invalid message handler key")]
InvalidMessageHandler,
#[msg("Invalid message account key")]
InvalidMessageAccountKey,
#[msg("Not read-only message account key")]
NotReadOnlyMessageAccountKey,
#[msg("Not writable message account key")]
NotWritableMessageAccountKey,
#[msg("Missing value recipient key")]
MissingValueRecipientKey,
}
9 changes: 7 additions & 2 deletions programs/svm-spoke/src/instructions/fill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
event::{FillType, FilledV3Relay, V3RelayExecutionEventInfo},
get_current_time,
state::{FillStatus, FillStatusAccount, State},
utils::invoke_handler,
};

#[event_cpi]
Expand Down Expand Up @@ -66,8 +67,8 @@ pub struct FillV3Relay<'info> {
pub system_program: Program<'info, System>,
}

pub fn fill_v3_relay(
ctx: Context<FillV3Relay>,
pub fn fill_v3_relay<'info>(
ctx: Context<'_, '_, '_, 'info, FillV3Relay<'info>>,
relay_data: V3RelayData,
repayment_chain_id: u64,
repayment_address: Pubkey,
Expand Down Expand Up @@ -123,6 +124,10 @@ pub fn fill_v3_relay(
// Emit the FilledV3Relay event
let message_clone = relay_data.message.clone(); // Clone the message before it is moved

if message_clone.len() > 0 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is slightly different than in EVM where we also check if the recipient is program. On Solana that would not be possible as the recipient is handler PDA, not the handler program itself. But I'm not sure if there would be a real use case for arbitrary messages that are not to be invoked by the handler.

invoke_handler(ctx.accounts.signer.as_ref(), ctx.remaining_accounts, &message_clone)?;
}

emit_cpi!(FilledV3Relay {
input_token: relay_data.input_token,
output_token: relay_data.output_token,
Expand Down
10 changes: 7 additions & 3 deletions programs/svm-spoke/src/instructions/slow_fill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
error::{CommonError, SvmError},
get_current_time,
state::{FillStatus, FillStatusAccount, RootBundle, State},
utils::verify_merkle_proof,
utils::{invoke_handler, verify_merkle_proof},
};

use crate::event::{FillType, FilledV3Relay, RequestedV3SlowFill, V3RelayExecutionEventInfo};
Expand Down Expand Up @@ -166,8 +166,8 @@ pub struct ExecuteV3SlowRelayLeaf<'info> {
pub system_program: Program<'info, System>,
}

pub fn execute_v3_slow_relay_leaf(
ctx: Context<ExecuteV3SlowRelayLeaf>,
pub fn execute_v3_slow_relay_leaf<'info>(
ctx: Context<'_, '_, '_, 'info, ExecuteV3SlowRelayLeaf<'info>>,
slow_fill_leaf: V3SlowFill,
proof: Vec<[u8; 32]>,
) -> Result<()> {
Expand Down Expand Up @@ -225,6 +225,10 @@ pub fn execute_v3_slow_relay_leaf(
// Emit the FilledV3Relay event
let message_clone = relay_data.message.clone(); // Clone the message before it is moved

if message_clone.len() > 0 {
invoke_handler(ctx.accounts.signer.as_ref(), ctx.remaining_accounts, &message_clone)?;
}

emit_cpi!(FilledV3Relay {
input_token: relay_data.input_token,
output_token: relay_data.output_token,
Expand Down
8 changes: 4 additions & 4 deletions programs/svm-spoke/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ pub mod svm_spoke {
}

// Relayer methods.
pub fn fill_v3_relay(
ctx: Context<FillV3Relay>,
pub fn fill_v3_relay<'info>(
ctx: Context<'_, '_, '_, 'info, FillV3Relay<'info>>,
_relay_hash: [u8; 32],
relay_data: V3RelayData,
repayment_chain_id: u64,
Expand Down Expand Up @@ -203,8 +203,8 @@ pub mod svm_spoke {
instructions::request_v3_slow_fill(ctx, relay_data)
}

pub fn execute_v3_slow_relay_leaf(
ctx: Context<ExecuteV3SlowRelayLeaf>,
pub fn execute_v3_slow_relay_leaf<'info>(
ctx: Context<'_, '_, '_, 'info, ExecuteV3SlowRelayLeaf<'info>>,
_relay_hash: [u8; 32],
slow_fill_leaf: V3SlowFill,
_root_bundle_id: u32,
Expand Down
102 changes: 102 additions & 0 deletions programs/svm-spoke/src/utils/message_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use anchor_lang::{
prelude::*,
solana_program::{instruction::Instruction, program::invoke, system_instruction},
};

use crate::{constants::DISCRIMINATOR_SIZE, error::AcrossPlusError};

// Sha256(global:handle_v3_across_message)[..8];
const HANDLE_V3_ACROSS_MESSAGE_DISCRIMINATOR: [u8; 8] = (0x838d3447103bc45c_u64).to_be_bytes();

#[derive(AnchorDeserialize)]
pub struct AcrossPlusMessage {
pub handler: Pubkey,
pub read_only_len: u8,
pub value_amount: u64,
pub accounts: Vec<Pubkey>,
pub handler_message: Vec<u8>,
}

pub fn invoke_handler<'info>(
relayer: &AccountInfo<'info>,
remaining_accounts: &[AccountInfo<'info>],
message: &Vec<u8>,
) -> Result<()> {
let message =
AcrossPlusMessage::deserialize(&mut &message[..]).map_err(|_| AcrossPlusError::MessageDidNotDeserialize)?;

// First remaining account is the handler and the rest are accounts to be passed to the message handler.
let message_accounts_len = message.accounts.len();
if remaining_accounts.len() != message_accounts_len + 1 {
return err!(AcrossPlusError::InvalidMessageKeyLength);
}
if (message.read_only_len as usize) > message_accounts_len {
return err!(AcrossPlusError::InvalidReadOnlyKeyLength);
}
let handler = &remaining_accounts[0];
let account_infos = &remaining_accounts[1..];

if handler.key() != message.handler {
return err!(AcrossPlusError::InvalidMessageHandler);
}

// Populate accounts for the invoked message handler CPI.
let mut accounts = Vec::with_capacity(message_accounts_len);
for (i, message_account_key) in message.accounts.into_iter().enumerate() {
if account_infos[i].key() != message_account_key {
return Err(Error::from(AcrossPlusError::InvalidMessageAccountKey)
.with_pubkeys((account_infos[i].key(), message_account_key)));
}

// Writable accounts must be passed first. This enforces the same write permissions as set in the message. Note
// that this would fail if any of mutable FillV3Relay / ExecuteV3SlowRelayLeaf accounts are passed as read-only
// in the bridged message as the calling client deduplicates the accounts and applies maximum required
// privileges. Though it is unlikely that any practical application would require this.
// We also explicitly disable all signer privileges for all the accounts to protect the relayer from being
// drained of funds in the inner instructions.
match i < message_accounts_len - (message.read_only_len as usize) {
true => {
if !account_infos[i].is_writable {
return Err(Error::from(AcrossPlusError::NotWritableMessageAccountKey)
.with_account_name(format!("{}", message_account_key)));
}
accounts.push(AccountMeta::new(message_account_key, false));
}
false => {
if account_infos[i].is_writable {
return Err(Error::from(AcrossPlusError::NotReadOnlyMessageAccountKey)
.with_account_name(format!("{}", message_account_key)));
}
accounts.push(AccountMeta::new_readonly(message_account_key, false));
}
}
}

// Transfer value amount from the relayer to the first account in the message accounts.
// Note that the depositor is responsible to make sure that after invoking the handler the recipient account will
// not hold any balance that is below its rent-exempt threshold, otherwise the fill would fail.
if message.value_amount > 0 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is mostly useful when invoked handler transaction requires account initialization payer (e.g. when creating ATAs)

let recipient_account = account_infos.get(0).ok_or(AcrossPlusError::MissingValueRecipientKey)?;
let transfer_ix = system_instruction::transfer(&relayer.key(), &recipient_account.key(), message.value_amount);
invoke(
&transfer_ix,
&[relayer.to_account_info(), recipient_account.to_account_info()],
)?;
}

// The data will hold the handler ix discriminator and raw handler message bytes (including 4 bytes for the length).
let mut data = Vec::with_capacity(DISCRIMINATOR_SIZE + 4 + message.handler_message.len());
data.extend_from_slice(&HANDLE_V3_ACROSS_MESSAGE_DISCRIMINATOR);
AnchorSerialize::serialize(&message.handler_message, &mut data)?;

let instruction = Instruction {
program_id: message.handler,
accounts,
data,
};

// TODO: consider if the message handler requires signed invocation.
Copy link
Contributor Author

@Reinis-FRP Reinis-FRP Nov 15, 2024

Choose a reason for hiding this comment

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

This mostly depends on whether the handler needs to retain any privileges on behalf of the user. E.g. if the handler performs a swap and forwards swapped-in tokens to user's ATA then its not important. On the other hand, if the handler deposits funds in a money market that does not allow depositing on behalf of end user then the handler would need to retain rights to withdraw the funds where such call must be authenticated via spoke pool PDA that is derived from depositing user address and origin chain.

invoke(&instruction, account_infos)?;

Ok(())
}
2 changes: 2 additions & 0 deletions programs/svm-spoke/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod bitmap_utils;
pub mod cctp_utils;
pub mod merkle_proof_utils;
pub mod message_utils;

pub use bitmap_utils::*;
pub use cctp_utils::*;
pub use merkle_proof_utils::*;
pub use message_utils::*;
Loading
Loading