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

Draft
wants to merge 11 commits into
base: master
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
3 changes: 2 additions & 1 deletion Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ resolution = true
skip-lint = false

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

Expand Down Expand Up @@ -35,7 +36,7 @@ remotePauseDeposits = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/remoteP
generateExternalTypes = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/generateExternalTypes.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 {
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 {
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 {
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.
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