Skip to content

Commit 58f2665

Browse files
md0xReinis-FRP
andauthored
fix(svm): M-01 Deposit Tokens Transferred from Depositor Token Account Instead of Signer (#971)
* fix(svm): M-01 Deposit Tokens Transfers Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * feat: use unchecked account Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * feat: remove system acc Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix: deposit tests Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix: fill tests Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * refactor: rename and comments Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix: across plus Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix(svm): pin rust toolchain for solana (#960) * fix(svm): pin rust toolchain for solana Signed-off-by: Reinis Martinsons <reinis@umaproject.org> * fix: add local toolchain Signed-off-by: Reinis Martinsons <reinis@umaproject.org> * fix: add rustfmt to nightly Signed-off-by: Reinis Martinsons <reinis@umaproject.org> * fix: pin nightly in lint scripts Signed-off-by: Reinis Martinsons <reinis@umaproject.org> --------- Signed-off-by: Reinis Martinsons <reinis@umaproject.org> * refactor: rename and organize function Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * feat: update deposit delegate seed Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * feat: use relay_hash from function arguments Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix: heap memory error Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * refactor: cleanup Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix: deposit checks Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix: fill tests Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix: fill relay delagate Signed-off-by: Pablo Maldonado <pablo@umaproject.org> * fix: fill Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * refactor: simplify Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * refactor: cleanup Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * test: update fill tests Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * refactor: comments Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * fix: scripts Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * refactor: make seed structs private Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * feat: add missing params to deposit hashes Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * refactor: simplify Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * refactor: delegate utils Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * refactor: anchor serialize Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * refactor: reuse helper deriveSeedHash Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * fix: move paused fills check in handler Signed-off-by: Reinis Martinsons <reinis@umaproject.org> * feat: improvements Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * fix: remove program_id from transfer_from params Signed-off-by: Reinis Martinsons <reinis@umaproject.org> * fix: fill import Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> * fix: tests Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> --------- Signed-off-by: Pablo Maldonado <pablo@umaproject.org> Signed-off-by: Reinis Martinsons <reinis@umaproject.org> Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> Co-authored-by: Reinis Martinsons <77973553+Reinis-FRP@users.noreply.github.com> Co-authored-by: Reinis Martinsons <reinis@umaproject.org>
1 parent 86553d7 commit 58f2665

File tree

14 files changed

+651
-158
lines changed

14 files changed

+651
-158
lines changed

programs/svm-spoke/src/error.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use anchor_lang::prelude::*;
55
pub enum CommonError {
66
#[msg("Invalid quote timestamp!")]
77
InvalidQuoteTimestamp,
8-
#[msg("Ivalid fill deadline!")]
8+
#[msg("Invalid fill deadline!")]
99
InvalidFillDeadline,
1010
#[msg("Caller is not the exclusive relayer and exclusivity deadline has not passed!")]
1111
NotExclusiveRelayer,
@@ -72,6 +72,8 @@ pub enum SvmError {
7272
InvalidProductionSeed,
7373
#[msg("Invalid remaining accounts for ATA creation!")]
7474
InvalidATACreationAccounts,
75+
#[msg("Invalid delegate PDA!")]
76+
InvalidDelegatePda,
7577
}
7678

7779
// CCTP specific errors.

programs/svm-spoke/src/instructions/deposit.rs

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use crate::{
1111
error::{CommonError, SvmError},
1212
event::FundsDeposited,
1313
state::State,
14-
utils::{get_current_time, get_unsafe_deposit_id, transfer_from},
14+
utils::{
15+
derive_seed_hash, get_current_time, get_unsafe_deposit_id, transfer_from, DepositNowSeedData, DepositSeedData,
16+
},
1517
};
1618

1719
#[event_cpi]
@@ -23,7 +25,7 @@ use crate::{
2325
output_token: Pubkey,
2426
input_amount: u64,
2527
output_amount: u64,
26-
destination_chain_id: u64,
28+
destination_chain_id: u64
2729
)]
2830
pub struct Deposit<'info> {
2931
#[account(mut)]
@@ -36,6 +38,9 @@ pub struct Deposit<'info> {
3638
)]
3739
pub state: Account<'info, State>,
3840

41+
/// CHECK: PDA derived with seeds ["delegate", seed_hash]; used as a CPI signer.
42+
pub delegate: UncheckedAccount<'info>,
43+
3944
#[account(
4045
mut,
4146
associated_token::mint = mint,
@@ -76,15 +81,14 @@ pub fn _deposit(
7681
fill_deadline: u32,
7782
exclusivity_parameter: u32,
7883
message: Vec<u8>,
84+
delegate_seed_hash: [u8; 32],
7985
) -> Result<()> {
8086
let state = &mut ctx.accounts.state;
81-
8287
let current_time = get_current_time(state)?;
8388

8489
if current_time.checked_sub(quote_timestamp).unwrap_or(u32::MAX) > state.deposit_quote_time_buffer {
8590
return err!(CommonError::InvalidQuoteTimestamp);
8691
}
87-
8892
if fill_deadline > current_time + state.fill_deadline_buffer {
8993
return err!(CommonError::InvalidFillDeadline);
9094
}
@@ -94,21 +98,20 @@ pub fn _deposit(
9498
if exclusivity_deadline <= MAX_EXCLUSIVITY_PERIOD_SECONDS {
9599
exclusivity_deadline += current_time;
96100
}
97-
98101
if exclusive_relayer == Pubkey::default() {
99102
return err!(CommonError::InvalidExclusiveRelayer);
100103
}
101104
}
102105

103-
// Depositor must have delegated input_amount to the state PDA.
106+
// Depositor must have delegated input_amount to the delegate PDA
104107
transfer_from(
105108
&ctx.accounts.depositor_token_account,
106109
&ctx.accounts.vault,
107110
input_amount,
108-
state,
109-
ctx.bumps.state,
111+
&ctx.accounts.delegate,
110112
&ctx.accounts.mint,
111113
&ctx.accounts.token_program,
114+
delegate_seed_hash,
112115
)?;
113116

114117
let mut applied_deposit_id = deposit_id;
@@ -152,6 +155,22 @@ pub fn deposit(
152155
exclusivity_parameter: u32,
153156
message: Vec<u8>,
154157
) -> Result<()> {
158+
let seed_hash = derive_seed_hash(
159+
&(DepositSeedData {
160+
depositor,
161+
recipient,
162+
input_token,
163+
output_token,
164+
input_amount,
165+
output_amount,
166+
destination_chain_id,
167+
exclusive_relayer,
168+
quote_timestamp,
169+
fill_deadline,
170+
exclusivity_parameter,
171+
message: &message,
172+
}),
173+
);
155174
_deposit(
156175
ctx,
157176
depositor,
@@ -167,6 +186,7 @@ pub fn deposit(
167186
fill_deadline,
168187
exclusivity_parameter,
169188
message,
189+
seed_hash,
170190
)?;
171191

172192
Ok(())
@@ -188,7 +208,22 @@ pub fn deposit_now(
188208
) -> Result<()> {
189209
let state = &mut ctx.accounts.state;
190210
let current_time = get_current_time(state)?;
191-
deposit(
211+
let seed_hash = derive_seed_hash(
212+
&(DepositNowSeedData {
213+
depositor,
214+
recipient,
215+
input_token,
216+
output_token,
217+
input_amount,
218+
output_amount,
219+
destination_chain_id,
220+
exclusive_relayer,
221+
fill_deadline_offset,
222+
exclusivity_period,
223+
message: &message,
224+
}),
225+
);
226+
_deposit(
192227
ctx,
193228
depositor,
194229
recipient,
@@ -198,10 +233,12 @@ pub fn deposit_now(
198233
output_amount,
199234
destination_chain_id,
200235
exclusive_relayer,
236+
ZERO_DEPOSIT_ID, // ZERO_DEPOSIT_ID informs internal function to use state.number_of_deposits as id.
201237
current_time,
202238
current_time + fill_deadline_offset,
203239
exclusivity_period,
204240
message,
241+
seed_hash,
205242
)?;
206243

207244
Ok(())
@@ -225,6 +262,22 @@ pub fn unsafe_deposit(
225262
) -> Result<()> {
226263
// Calculate the unsafe deposit ID as a [u8; 32]
227264
let deposit_id = get_unsafe_deposit_id(ctx.accounts.signer.key(), depositor, deposit_nonce);
265+
let seed_hash = derive_seed_hash(
266+
&(DepositSeedData {
267+
depositor,
268+
recipient,
269+
input_token,
270+
output_token,
271+
input_amount,
272+
output_amount,
273+
destination_chain_id,
274+
exclusive_relayer,
275+
quote_timestamp,
276+
fill_deadline,
277+
exclusivity_parameter,
278+
message: &message,
279+
}),
280+
);
228281
_deposit(
229282
ctx,
230283
depositor,
@@ -240,6 +293,7 @@ pub fn unsafe_deposit(
240293
fill_deadline,
241294
exclusivity_parameter,
242295
message,
296+
seed_hash,
243297
)?;
244298

245299
Ok(())

programs/svm-spoke/src/instructions/fill.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
error::{CommonError, SvmError},
1212
event::{FillType, FilledRelay, RelayExecutionEventInfo},
1313
state::{FillRelayParams, FillStatus, FillStatusAccount, State},
14-
utils::{get_current_time, hash_non_empty_message, invoke_handler, transfer_from},
14+
utils::{derive_seed_hash, get_current_time, hash_non_empty_message, invoke_handler, transfer_from, FillSeedData},
1515
};
1616

1717
#[event_cpi]
@@ -25,13 +25,12 @@ pub struct FillRelay<'info> {
2525
#[account(mut, seeds = [b"instruction_params", signer.key().as_ref()], bump, close = signer)]
2626
pub instruction_params: Option<Account<'info, FillRelayParams>>,
2727

28-
#[account(
29-
seeds = [b"state", state.seed.to_le_bytes().as_ref()],
30-
bump,
31-
constraint = !state.paused_fills @ CommonError::FillsArePaused
32-
)]
28+
#[account(seeds = [b"state", state.seed.to_le_bytes().as_ref()], bump)]
3329
pub state: Account<'info, State>,
3430

31+
/// CHECK: PDA derived with seeds ["delegate", seed_hash]; used as a CPI signer.
32+
pub delegate: UncheckedAccount<'info>,
33+
3534
#[account(
3635
mint::token_program = token_program,
3736
address = relay_data
@@ -81,10 +80,15 @@ pub struct FillRelay<'info> {
8180

8281
pub fn fill_relay<'info>(
8382
ctx: Context<'_, '_, '_, 'info, FillRelay<'info>>,
83+
relay_hash: [u8; 32],
8484
relay_data: Option<RelayData>,
8585
repayment_chain_id: Option<u64>,
8686
repayment_address: Option<Pubkey>,
8787
) -> Result<()> {
88+
// This type of constraint normally would be checked in the context, but had to move it here in the handler to avoid
89+
// exceeding maximum stack offset.
90+
require!(!ctx.accounts.state.paused_fills, CommonError::FillsArePaused);
91+
8892
let FillRelayParams { relay_data, repayment_chain_id, repayment_address } =
8993
unwrap_fill_relay_params(relay_data, repayment_chain_id, repayment_address, &ctx.accounts.instruction_params);
9094

@@ -114,15 +118,17 @@ pub fn fill_relay<'info>(
114118
_ => FillType::FastFill,
115119
};
116120

117-
// Relayer must have delegated output_amount to the state PDA
121+
let seed_hash = derive_seed_hash(&(FillSeedData { relay_hash, repayment_chain_id, repayment_address }));
122+
123+
// Relayer must have delegated output_amount to the delegate PDA
118124
transfer_from(
119125
&ctx.accounts.relayer_token_account,
120126
&ctx.accounts.recipient_token_account,
121127
relay_data.output_amount,
122-
state,
123-
ctx.bumps.state,
128+
&ctx.accounts.delegate,
124129
&ctx.accounts.mint,
125130
&ctx.accounts.token_program,
131+
seed_hash,
126132
)?;
127133

128134
// Update the fill status to Filled, set the relayer and fill deadline

programs/svm-spoke/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ pub mod svm_spoke {
205205
/// Authority must be the state.
206206
/// - mint (Account): The mint account for the input token.
207207
/// - token_program (Interface): The token program.
208+
/// - delegate (Account): The account used to delegate the input amount of the input token.
208209
///
209210
/// ### Parameters
210211
/// - depositor: The account credited with the deposit. Can be different from the signer.
@@ -377,9 +378,10 @@ pub mod svm_spoke {
377378
/// - token_program (Interface): The token program.
378379
/// - associated_token_program (Interface): The associated token program.
379380
/// - system_program (Interface): The system program.
381+
/// - delegate (Account): The account used to delegate the output amount of the output token.
380382
///
381383
/// ### Parameters:
382-
/// - _relay_hash: The hash identifying the deposit to be filled. Caller must pass this in. Computed as hash of
384+
/// - relay_hash: The hash identifying the deposit to be filled. Caller must pass this in. Computed as hash of
383385
/// the flattened relay_data & destination_chain_id.
384386
/// - relay_data: Struct containing all the data needed to identify the deposit to be filled. Should match
385387
/// all the same-named parameters emitted in the origin chain FundsDeposited event.
@@ -406,12 +408,12 @@ pub mod svm_spoke {
406408
/// is passed, the caller must load them via the instruction_params account.
407409
pub fn fill_relay<'info>(
408410
ctx: Context<'_, '_, '_, 'info, FillRelay<'info>>,
409-
_relay_hash: [u8; 32],
411+
relay_hash: [u8; 32],
410412
relay_data: Option<RelayData>,
411413
repayment_chain_id: Option<u64>,
412414
repayment_address: Option<Pubkey>,
413415
) -> Result<()> {
414-
instructions::fill_relay(ctx, relay_data, repayment_chain_id, repayment_address)
416+
instructions::fill_relay(ctx, relay_hash, relay_data, repayment_chain_id, repayment_address)
415417
}
416418

417419
/// Closes the FillStatusAccount PDA to reclaim relayer rent.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use anchor_lang::{prelude::*, solana_program::keccak};
2+
3+
pub fn derive_seed_hash<T: AnchorSerialize>(seed: &T) -> [u8; 32] {
4+
let mut data = Vec::new();
5+
AnchorSerialize::serialize(seed, &mut data).unwrap();
6+
keccak::hash(&data).to_bytes()
7+
}
8+
9+
#[derive(AnchorSerialize)]
10+
pub struct DepositSeedData<'a> {
11+
pub depositor: Pubkey,
12+
pub recipient: Pubkey,
13+
pub input_token: Pubkey,
14+
pub output_token: Pubkey,
15+
pub input_amount: u64,
16+
pub output_amount: u64,
17+
pub destination_chain_id: u64,
18+
pub exclusive_relayer: Pubkey,
19+
pub quote_timestamp: u32,
20+
pub fill_deadline: u32,
21+
pub exclusivity_parameter: u32,
22+
pub message: &'a Vec<u8>,
23+
}
24+
25+
#[derive(AnchorSerialize)]
26+
pub struct DepositNowSeedData<'a> {
27+
pub depositor: Pubkey,
28+
pub recipient: Pubkey,
29+
pub input_token: Pubkey,
30+
pub output_token: Pubkey,
31+
pub input_amount: u64,
32+
pub output_amount: u64,
33+
pub destination_chain_id: u64,
34+
pub exclusive_relayer: Pubkey,
35+
pub fill_deadline_offset: u32,
36+
pub exclusivity_period: u32,
37+
pub message: &'a Vec<u8>,
38+
}
39+
40+
#[derive(AnchorSerialize)]
41+
pub struct FillSeedData {
42+
pub relay_hash: [u8; 32],
43+
pub repayment_chain_id: u64,
44+
pub repayment_address: Pubkey,
45+
}

programs/svm-spoke/src/utils/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod bitmap_utils;
22
pub mod cctp_utils;
3+
pub mod delegate_utils;
34
pub mod deposit_utils;
45
pub mod merkle_proof_utils;
56
pub mod message_utils;
@@ -8,6 +9,7 @@ pub mod transfer_utils;
89

910
pub use bitmap_utils::*;
1011
pub use cctp_utils::*;
12+
pub use delegate_utils::*;
1113
pub use deposit_utils::*;
1214
pub use merkle_proof_utils::*;
1315
pub use message_utils::*;

programs/svm-spoke/src/utils/transfer_utils.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1+
use crate::{error::SvmError, program::SvmSpoke};
12
use anchor_lang::prelude::*;
23
use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked};
34

4-
use crate::State;
5-
65
pub fn transfer_from<'info>(
76
from: &InterfaceAccount<'info, TokenAccount>,
87
to: &InterfaceAccount<'info, TokenAccount>,
98
amount: u64,
10-
state: &Account<'info, State>,
11-
state_bump: u8,
9+
delegate: &UncheckedAccount<'info>,
1210
mint: &InterfaceAccount<'info, Mint>,
1311
token_program: &Interface<'info, TokenInterface>,
12+
delegate_seed_hash: [u8; 32],
1413
) -> Result<()> {
14+
let (pda, bump) = Pubkey::find_program_address(&[b"delegate", &delegate_seed_hash], &SvmSpoke::id());
15+
if pda != delegate.key() {
16+
return err!(SvmError::InvalidDelegatePda);
17+
}
18+
let seeds: &[&[u8]] = &[b"delegate".as_ref(), &delegate_seed_hash, &[bump]];
19+
let signer_seeds: &[&[&[u8]]] = &[seeds];
1520
let transfer_accounts = TransferChecked {
1621
from: from.to_account_info(),
1722
mint: mint.to_account_info(),
1823
to: to.to_account_info(),
19-
authority: state.to_account_info(),
24+
authority: delegate.to_account_info(),
2025
};
21-
22-
let state_seed_bytes = state.seed.to_le_bytes();
23-
let seeds = &[b"state", state_seed_bytes.as_ref(), &[state_bump]];
24-
let signer_seeds = &[&seeds[..]];
25-
2626
let cpi_context = CpiContext::new_with_signer(token_program.to_account_info(), transfer_accounts, signer_seeds);
2727

2828
transfer_checked(cpi_context, amount, mint.decimals)

0 commit comments

Comments
 (0)