-
Notifications
You must be signed in to change notification settings - Fork 50
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
base: master
Are you sure you want to change the base?
Changes from all commits
6af8108
eff16eb
00e2a42
5ea1625
03a87dd
cedbd26
d1d0b01
cb7e421
4bd28bb
b1fab92
a8c6f53
b9b7732
23d454e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[target.bpfel-unknown-unknown.dependencies.std] | ||
features = [] |
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 { | ||
pub program_id_index: u8, | ||
pub account_key_indexes: Vec<u8>, | ||
pub data: Vec<u8>, | ||
} | ||
|
||
#[derive(Accounts)] | ||
pub struct HandleV3AcrossMessage {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ use crate::{ | |
event::{FillType, FilledV3Relay, V3RelayExecutionEventInfo}, | ||
get_current_time, | ||
state::{FillStatus, FillStatusAccount, State}, | ||
utils::invoke_handler, | ||
}; | ||
|
||
#[event_cpi] | ||
|
@@ -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, | ||
|
@@ -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 { | ||
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 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, | ||
|
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 { | ||
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 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. | ||
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 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(()) | ||
} |
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::*; |
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 resembles compiled instructions within a transaction message, except we don't use the
compact-u16
encoding for the sake of code simplicity.