Skip to content

fix(svm): M-01 Deposit Tokens Transferred from Depositor Token Account Instead of Signer #958

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

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0e9be37
fix(svm): M-01 Deposit Tokens Transfers
md0x Apr 16, 2025
a776129
feat: use unchecked account
md0x Apr 17, 2025
473abaf
feat: remove system acc
md0x Apr 17, 2025
46e561e
fix: deposit tests
md0x Apr 17, 2025
44aaac3
fix: fill tests
md0x Apr 17, 2025
7aa3d8a
refactor: rename and comments
md0x Apr 17, 2025
6d86ec4
fix: across plus
md0x Apr 17, 2025
6ff9e4b
Merge branch 'master' into pablo/acx-4021-m-01-deposit-tokens-transfe…
md0x Apr 17, 2025
b6eab4b
refactor: rename and organize function
md0x Apr 17, 2025
b45f24d
feat: update deposit delegate seed
md0x Apr 17, 2025
101c4b6
feat: use relay_hash from function arguments
md0x Apr 18, 2025
26f59ca
fix: heap memory error
md0x Apr 18, 2025
71cf1ba
fix
md0x Apr 18, 2025
3ef6c95
refactor: cleanup
md0x Apr 18, 2025
a324daf
fix: deposit checks
md0x Apr 18, 2025
6671830
fix: fill tests
md0x Apr 19, 2025
81b6cf5
fix: fill relay delagate
md0x Apr 19, 2025
6e01b20
fix: fill
md0x Apr 20, 2025
6a63040
refactor: simplify
md0x Apr 20, 2025
ef08497
refactor: cleanup
md0x Apr 20, 2025
6bad227
refactor: clean fill test
md0x Apr 20, 2025
659254f
test: update fill tests
md0x Apr 20, 2025
9f43af0
refactor: comments
md0x Apr 20, 2025
8431833
fix: scripts
md0x Apr 20, 2025
b6b8b9e
refactor: make seed structs private
md0x Apr 21, 2025
5d34e89
feat: add missing params to deposit hashes
md0x Apr 21, 2025
23cafcc
refactor: simplify
md0x Apr 21, 2025
5c0f8be
refactor: delegate utils
md0x Apr 21, 2025
be4401e
refactor: anchor serialize
md0x Apr 21, 2025
4150839
refactor: reuse helper deriveSeedHash
md0x Apr 21, 2025
28bf1e1
fix: move paused fills check in handler
Reinis-FRP Apr 22, 2025
e475b31
feat: improvements
md0x Apr 22, 2025
9acb2ff
fix: remove program_id from transfer_from params
Reinis-FRP Apr 22, 2025
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
28 changes: 21 additions & 7 deletions programs/svm-spoke/src/instructions/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
error::{CommonError, SvmError},
event::FundsDeposited,
state::{Route, State},
utils::{get_current_time, get_unsafe_deposit_id, transfer_from},
utils::{derive_delegate_seed_hash, get_current_time, get_unsafe_deposit_id, transfer_from},
};

#[event_cpi]
Expand All @@ -36,6 +36,17 @@ pub struct Deposit<'info> {
)]
pub state: Account<'info, State>,

#[account(
seeds = [
b"delegate",
state.seed.to_le_bytes().as_ref(),
&derive_delegate_seed_hash(input_token, output_token, input_amount, output_amount, destination_chain_id),
],
bump
)]
/// CHECK: PDA derived with seeds ["delegate", state.seed, delegate_seed_hash]; used as a CPI signer. No account data is read or written.
pub delegate: UncheckedAccount<'info>,

#[account(
seeds = [b"route", input_token.as_ref(), state.seed.to_le_bytes().as_ref(), destination_chain_id.to_le_bytes().as_ref()],
bump,
Expand Down Expand Up @@ -85,13 +96,11 @@ pub fn _deposit(
message: Vec<u8>,
) -> Result<()> {
let state = &mut ctx.accounts.state;

let current_time = get_current_time(state)?;

if current_time.checked_sub(quote_timestamp).unwrap_or(u32::MAX) > state.deposit_quote_time_buffer {
return err!(CommonError::InvalidQuoteTimestamp);
}

if fill_deadline > current_time + state.fill_deadline_buffer {
return err!(CommonError::InvalidFillDeadline);
}
Expand All @@ -101,21 +110,26 @@ pub fn _deposit(
if exclusivity_deadline <= MAX_EXCLUSIVITY_PERIOD_SECONDS {
exclusivity_deadline += current_time;
}

if exclusive_relayer == Pubkey::default() {
return err!(CommonError::InvalidExclusiveRelayer);
}
}

// Depositor must have delegated input_amount to the state PDA.
let bump = ctx.bumps.delegate;
let delegate_hash =
derive_delegate_seed_hash(input_token, output_token, input_amount, output_amount, destination_chain_id);
let state_seed_bytes = state.seed.to_le_bytes();
let signer_seeds: &[&[u8]] = &[b"delegate", &state_seed_bytes, &delegate_hash, &[bump]];

// Relayer must have delegated output_amount to the delegate PDA
transfer_from(
&ctx.accounts.depositor_token_account,
&ctx.accounts.vault,
input_amount,
state,
ctx.bumps.state,
&ctx.accounts.delegate,
&ctx.accounts.mint,
&ctx.accounts.token_program,
signer_seeds,
)?;

let mut applied_deposit_id = deposit_id;
Expand Down
15 changes: 12 additions & 3 deletions programs/svm-spoke/src/instructions/fill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub struct FillRelay<'info> {
)]
pub state: Account<'info, State>,

#[account(seeds = [b"delegate", state.seed.to_le_bytes().as_ref(), relay_hash.as_ref()], bump)]
/// CHECK: PDA derived with seeds ["delegate", state.seed, relay_hash]; used as a CPI signer. No account data is read or written.
pub delegate: UncheckedAccount<'info>,

#[account(
mint::token_program = token_program,
address = relay_data
Expand Down Expand Up @@ -81,6 +85,7 @@ pub struct FillRelay<'info> {

pub fn fill_relay<'info>(
ctx: Context<'_, '_, '_, 'info, FillRelay<'info>>,
relay_hash: [u8; 32],
relay_data: Option<RelayData>,
repayment_chain_id: Option<u64>,
repayment_address: Option<Pubkey>,
Expand Down Expand Up @@ -114,15 +119,19 @@ pub fn fill_relay<'info>(
_ => FillType::FastFill,
};

// Relayer must have delegated output_amount to the state PDA
let bump = ctx.bumps.delegate;
let state_seed_bytes = state.seed.to_le_bytes();
let signer_seeds: &[&[u8]] = &[b"delegate", &state_seed_bytes, &relay_hash, &[bump]];

// Relayer must have delegated output_amount to the delegate PDA
transfer_from(
&ctx.accounts.relayer_token_account,
&ctx.accounts.recipient_token_account,
relay_data.output_amount,
state,
ctx.bumps.state,
&ctx.accounts.delegate,
&ctx.accounts.mint,
&ctx.accounts.token_program,
signer_seeds,
)?;

// Update the fill status to Filled, set the relayer and fill deadline
Expand Down
6 changes: 3 additions & 3 deletions programs/svm-spoke/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ pub mod svm_spoke {
/// - system_program (Interface): The system program.
///
/// ### Parameters:
/// - _relay_hash: The hash identifying the deposit to be filled. Caller must pass this in. Computed as hash of
/// - relay_hash: The hash identifying the deposit to be filled. Caller must pass this in. Computed as hash of
/// the flattened relay_data & destination_chain_id.
/// - relay_data: Struct containing all the data needed to identify the deposit to be filled. Should match
/// all the same-named parameters emitted in the origin chain FundsDeposited event.
Expand All @@ -440,12 +440,12 @@ pub mod svm_spoke {
/// is passed, the caller must load them via the instruction_params account.
pub fn fill_relay<'info>(
ctx: Context<'_, '_, '_, 'info, FillRelay<'info>>,
_relay_hash: [u8; 32],
relay_hash: [u8; 32],
relay_data: Option<RelayData>,
repayment_chain_id: Option<u64>,
repayment_address: Option<Pubkey>,
) -> Result<()> {
instructions::fill_relay(ctx, relay_data, repayment_chain_id, repayment_address)
instructions::fill_relay(ctx, relay_hash, relay_data, repayment_chain_id, repayment_address)
}

/// Closes the FillStatusAccount PDA to reclaim relayer rent.
Expand Down
16 changes: 16 additions & 0 deletions programs/svm-spoke/src/utils/deposit_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,19 @@ pub fn get_unsafe_deposit_id(msg_sender: Pubkey, depositor: Pubkey, deposit_nonc

keccak::hash(&data).to_bytes()
}

pub fn derive_delegate_seed_hash(
input_token: Pubkey,
output_token: Pubkey,
input_amount: u64,
output_amount: u64,
destination_chain_id: u64,
) -> [u8; 32] {
let mut data = Vec::with_capacity(32 + 32 + 8 + 8 + 8);
data.extend_from_slice(input_token.as_ref());
data.extend_from_slice(output_token.as_ref());
data.extend_from_slice(&input_amount.to_le_bytes());
data.extend_from_slice(&output_amount.to_le_bytes());
data.extend_from_slice(&destination_chain_id.to_le_bytes());
keccak::hash(&data).to_bytes()
}
16 changes: 6 additions & 10 deletions programs/svm-spoke/src/utils/transfer_utils.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked};

use crate::State;

pub fn transfer_from<'info>(
from: &InterfaceAccount<'info, TokenAccount>,
to: &InterfaceAccount<'info, TokenAccount>,
amount: u64,
state: &Account<'info, State>,
state_bump: u8,
delegate: &UncheckedAccount<'info>,
mint: &InterfaceAccount<'info, Mint>,
token_program: &Interface<'info, TokenInterface>,
signer_seeds: &[&[u8]],
) -> Result<()> {
let transfer_accounts = TransferChecked {
from: from.to_account_info(),
mint: mint.to_account_info(),
to: to.to_account_info(),
authority: state.to_account_info(),
authority: delegate.to_account_info(),
};

let state_seed_bytes = state.seed.to_le_bytes();
let seeds = &[b"state", state_seed_bytes.as_ref(), &[state_bump]];
let signer_seeds = &[&seeds[..]];

let cpi_context = CpiContext::new_with_signer(token_program.to_account_info(), transfer_accounts, signer_seeds);
let signer_seeds_slice = &[signer_seeds];
let cpi_context =
CpiContext::new_with_signer(token_program.to_account_info(), transfer_accounts, signer_seeds_slice);

transfer_checked(cpi_context, amount, mint.decimals)
}
33 changes: 32 additions & 1 deletion src/svm/web3-v1/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AnchorProvider } from "@coral-xyz/anchor";
import { AnchorProvider, BN } from "@coral-xyz/anchor";
import { BigNumber } from "@ethersproject/bignumber";
import { ethers } from "ethers";
import { DepositData } from "../../types/svm";
import { PublicKey } from "@solana/web3.js";

/**
* Returns the chainId for a given solana cluster.
Expand All @@ -20,3 +22,32 @@ export const isSolanaDevnet = (provider: AnchorProvider): boolean => {
else if (solanaRpcEndpoint.includes("mainnet")) return false;
else throw new Error(`Unsupported solanaCluster endpoint: ${solanaRpcEndpoint}`);
};

/**
* Returns the delegate PDA for a deposit.
*/
export const getDepositDelegatePda = (depositData: DepositData, stateSeed: BN, programId: PublicKey) => {
const raw = Buffer.concat([
depositData.inputToken!.toBytes(),
depositData.outputToken.toBytes(),
depositData.inputAmount.toArrayLike(Buffer, "le", 8),
depositData.outputAmount.toArrayLike(Buffer, "le", 8),
depositData.destinationChainId.toArrayLike(Buffer, "le", 8),
]);
const hashHex = ethers.utils.keccak256(raw);
const seedHash = Buffer.from(hashHex.slice(2), "hex");
return PublicKey.findProgramAddressSync(
[Buffer.from("delegate"), stateSeed.toArrayLike(Buffer, "le", 8), seedHash],
programId
)[0];
};

/**
* Returns the delegate PDA for a fill relay.
*/
export const getFillRelayDelegatePda = (relayHash: Uint8Array, stateSeed: BN, programId: PublicKey) => {
return PublicKey.findProgramAddressSync(
[Buffer.from("delegate"), stateSeed.toArrayLike(Buffer, "le", 8), relayHash],
programId
)[0];
};
Loading
Loading