Skip to content

Commit d6497e3

Browse files
md0xReinis-FRP
andauthored
fix(svm): M-01 Deposit Tokens Transferred from Depositor Token Account Instead of Signer (#958)
* 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> * 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> * refactor: clean fill test 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> --------- Signed-off-by: Pablo Maldonado <pablo@umaproject.org> Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com> Signed-off-by: Reinis Martinsons <reinis@umaproject.org> Co-authored-by: Reinis Martinsons <reinis@umaproject.org>
1 parent 5ec5e5a commit d6497e3

File tree

14 files changed

+684
-169
lines changed

14 files changed

+684
-169
lines changed

programs/svm-spoke/src/error.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub enum CommonError {
77
DisabledRoute,
88
#[msg("Invalid quote timestamp!")]
99
InvalidQuoteTimestamp,
10-
#[msg("Ivalid fill deadline!")]
10+
#[msg("Invalid fill deadline!")]
1111
InvalidFillDeadline,
1212
#[msg("Caller is not the exclusive relayer and exclusivity deadline has not passed!")]
1313
NotExclusiveRelayer,
@@ -74,6 +74,8 @@ pub enum SvmError {
7474
InvalidProductionSeed,
7575
#[msg("Invalid remaining accounts for ATA creation!")]
7676
InvalidATACreationAccounts,
77+
#[msg("Invalid delegate PDA!")]
78+
InvalidDelegatePda,
7779
}
7880

7981
// 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::{Route, 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
seeds = [b"route", input_token.as_ref(), state.seed.to_le_bytes().as_ref(), destination_chain_id.to_le_bytes().as_ref()],
4146
bump,
@@ -83,15 +88,14 @@ pub fn _deposit(
8388
fill_deadline: u32,
8489
exclusivity_parameter: u32,
8590
message: Vec<u8>,
91+
delegate_seed_hash: [u8; 32],
8692
) -> Result<()> {
8793
let state = &mut ctx.accounts.state;
88-
8994
let current_time = get_current_time(state)?;
9095

9196
if current_time.checked_sub(quote_timestamp).unwrap_or(u32::MAX) > state.deposit_quote_time_buffer {
9297
return err!(CommonError::InvalidQuoteTimestamp);
9398
}
94-
9599
if fill_deadline > current_time + state.fill_deadline_buffer {
96100
return err!(CommonError::InvalidFillDeadline);
97101
}
@@ -101,21 +105,20 @@ pub fn _deposit(
101105
if exclusivity_deadline <= MAX_EXCLUSIVITY_PERIOD_SECONDS {
102106
exclusivity_deadline += current_time;
103107
}
104-
105108
if exclusive_relayer == Pubkey::default() {
106109
return err!(CommonError::InvalidExclusiveRelayer);
107110
}
108111
}
109112

110-
// Depositor must have delegated input_amount to the state PDA.
113+
// Depositor must have delegated input_amount to the delegate PDA
111114
transfer_from(
112115
&ctx.accounts.depositor_token_account,
113116
&ctx.accounts.vault,
114117
input_amount,
115-
state,
116-
ctx.bumps.state,
118+
&ctx.accounts.delegate,
117119
&ctx.accounts.mint,
118120
&ctx.accounts.token_program,
121+
delegate_seed_hash,
119122
)?;
120123

121124
let mut applied_deposit_id = deposit_id;
@@ -159,6 +162,22 @@ pub fn deposit(
159162
exclusivity_parameter: u32,
160163
message: Vec<u8>,
161164
) -> Result<()> {
165+
let seed_hash = derive_seed_hash(
166+
&(DepositSeedData {
167+
depositor,
168+
recipient,
169+
input_token,
170+
output_token,
171+
input_amount,
172+
output_amount,
173+
destination_chain_id,
174+
exclusive_relayer,
175+
quote_timestamp,
176+
fill_deadline,
177+
exclusivity_parameter,
178+
message: &message,
179+
}),
180+
);
162181
_deposit(
163182
ctx,
164183
depositor,
@@ -174,6 +193,7 @@ pub fn deposit(
174193
fill_deadline,
175194
exclusivity_parameter,
176195
message,
196+
seed_hash,
177197
)?;
178198

179199
Ok(())
@@ -195,7 +215,22 @@ pub fn deposit_now(
195215
) -> Result<()> {
196216
let state = &mut ctx.accounts.state;
197217
let current_time = get_current_time(state)?;
198-
deposit(
218+
let seed_hash = derive_seed_hash(
219+
&(DepositNowSeedData {
220+
depositor,
221+
recipient,
222+
input_token,
223+
output_token,
224+
input_amount,
225+
output_amount,
226+
destination_chain_id,
227+
exclusive_relayer,
228+
fill_deadline_offset,
229+
exclusivity_period,
230+
message: &message,
231+
}),
232+
);
233+
_deposit(
199234
ctx,
200235
depositor,
201236
recipient,
@@ -205,10 +240,12 @@ pub fn deposit_now(
205240
output_amount,
206241
destination_chain_id,
207242
exclusive_relayer,
243+
ZERO_DEPOSIT_ID, // ZERO_DEPOSIT_ID informs internal function to use state.number_of_deposits as id.
208244
current_time,
209245
current_time + fill_deadline_offset,
210246
exclusivity_period,
211247
message,
248+
seed_hash,
212249
)?;
213250

214251
Ok(())
@@ -232,6 +269,22 @@ pub fn unsafe_deposit(
232269
) -> Result<()> {
233270
// Calculate the unsafe deposit ID as a [u8; 32]
234271
let deposit_id = get_unsafe_deposit_id(ctx.accounts.signer.key(), depositor, deposit_nonce);
272+
let seed_hash = derive_seed_hash(
273+
&(DepositSeedData {
274+
depositor,
275+
recipient,
276+
input_token,
277+
output_token,
278+
input_amount,
279+
output_amount,
280+
destination_chain_id,
281+
exclusive_relayer,
282+
quote_timestamp,
283+
fill_deadline,
284+
exclusivity_parameter,
285+
message: &message,
286+
}),
287+
);
235288
_deposit(
236289
ctx,
237290
depositor,
@@ -247,6 +300,7 @@ pub fn unsafe_deposit(
247300
fill_deadline,
248301
exclusivity_parameter,
249302
message,
303+
seed_hash,
250304
)?;
251305

252306
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
@@ -235,6 +235,7 @@ pub mod svm_spoke {
235235
/// Authority must be the state.
236236
/// - mint (Account): The mint account for the input token.
237237
/// - token_program (Interface): The token program.
238+
/// - delegate (Account): The account used to delegate the input amount of the input token.
238239
///
239240
/// ### Parameters
240241
/// - depositor: The account credited with the deposit. Can be different from the signer.
@@ -411,9 +412,10 @@ pub mod svm_spoke {
411412
/// - token_program (Interface): The token program.
412413
/// - associated_token_program (Interface): The associated token program.
413414
/// - system_program (Interface): The system program.
415+
/// - delegate (Account): The account used to delegate the output amount of the output token.
414416
///
415417
/// ### Parameters:
416-
/// - _relay_hash: The hash identifying the deposit to be filled. Caller must pass this in. Computed as hash of
418+
/// - relay_hash: The hash identifying the deposit to be filled. Caller must pass this in. Computed as hash of
417419
/// the flattened relay_data & destination_chain_id.
418420
/// - relay_data: Struct containing all the data needed to identify the deposit to be filled. Should match
419421
/// all the same-named parameters emitted in the origin chain FundsDeposited event.
@@ -440,12 +442,12 @@ pub mod svm_spoke {
440442
/// is passed, the caller must load them via the instruction_params account.
441443
pub fn fill_relay<'info>(
442444
ctx: Context<'_, '_, '_, 'info, FillRelay<'info>>,
443-
_relay_hash: [u8; 32],
445+
relay_hash: [u8; 32],
444446
relay_data: Option<RelayData>,
445447
repayment_chain_id: Option<u64>,
446448
repayment_address: Option<Pubkey>,
447449
) -> Result<()> {
448-
instructions::fill_relay(ctx, relay_data, repayment_chain_id, repayment_address)
450+
instructions::fill_relay(ctx, relay_hash, relay_data, repayment_chain_id, repayment_address)
449451
}
450452

451453
/// 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)