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

Open
wants to merge 24 commits into
base: master
Choose a base branch
from

Conversation

md0x
Copy link
Contributor

@md0x md0x commented Apr 16, 2025

OZ identified the followin issue:

A deposit action to the SpokePool program is meant to [pull tokens from the caller](https://github.com/across-protocol/contracts/blob/71e990b3f908ecf994be8723e041b584c7d49318/programs/svm-spoke/src/lib.rs#L213) (i.e., the [signer](https://github.com/across-protocol/contracts/blob/71e990b3f908ecf994be8723e041b584c7d49318/programs/svm-spoke/src/instructions/deposit.rs#L30)) which may be deposited on behalf of any depositor account. However, when the signer is not the depositor, a transfer_from operation is performed with the [depositor token account as the source address](https://github.com/across-protocol/contracts/blob/71e990b3f908ecf994be8723e041b584c7d49318/programs/svm-spoke/src/instructions/deposit.rs#L105). Such an operation would fail unless the depositor had delegated at least the input amount to the state PDA. This presents two consequences:

The signer will not be able to deposit for another account, disabling the intended feature of an [account being able to deposit on behalf of someone else](https://github.com/across-protocol/contracts/blob/71e990b3f908ecf994be8723e041b584c7d49318/programs/svm-spoke/src/lib.rs#L210).

If any token account delegates to the state PDA, then it is possible for anyone to call the deposit function, passing the victim's account as the [depositor_token_account](https://github.com/across-protocol/contracts/blob/71e990b3f908ecf994be8723e041b584c7d49318/programs/svm-spoke/src/instructions/deposit.rs#L45) and performing a successful deposit with arbitrary arguments other than the input token and amount. A malicious user could then submit a deposit with their own recipient address to steal depositor funds. This is possible due to the state PDA being passed as the [authority and signer seed](https://github.com/across-protocol/contracts/blob/71e990b3f908ecf994be8723e041b584c7d49318/programs/svm-spoke/src/utils/transfer_utils.rs#L19-L28) to the transfer_checked call, which, when delegated to, will pass [the transfer validation logic](https://docs.rs/spl-token/latest/src/spl_token/processor.rs.html#274). Note that this scenario is mitigated by the fact that native instruction batching in Solana transactions would typically involve delegation and transferring in one operation, making such a front-running scenario less likely but still possible.

Consider validating the signer token account in the same way as depositor_token_account, and performing the transfer from the signer token account.

We replaced the one “state” PDA with two distinct PDAs—one for deposit and one for fill_relay. Now users must explicitly delegate to the correct PDA before anyone can pull their tokens, restoring safe third‑party deposits and eliminating the risk of a single authority being misused to steal funds.

md0x added 9 commits April 17, 2025 14:38
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
@md0x md0x marked this pull request as ready for review April 17, 2025 18:31
@md0x md0x changed the title fix(svm): M-01 Deposit Tokens Transfers fix(svm): Deposit Tokens Transferred from Depositor Token Account Instead of Signer Apr 18, 2025
@md0x md0x changed the title fix(svm): Deposit Tokens Transferred from Depositor Token Account Instead of Signer fix(svm): M-01 Deposit Tokens Transferred from Depositor Token Account Instead of Signer Apr 18, 2025
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Relayer must have delegated output_amount to the delegate PDA
// Depositor must have delegated input_amount to the delegate PDA

Comment on lines 118 to 122
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]];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be cleaner to move seed derivation back in the transfer_from helper, just pass additional delegate hash and bump to it

#[account(
seeds = [
b"delegate",
state.seed.to_le_bytes().as_ref(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do tests depend on this? if not, we better drop the state seed here as its 0 on public networks

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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this still would allow someone to steal delegation from separate tx by replacing recipient and message. I think it would be safer and more consistent if we hashed all deposit parameters

@@ -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)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this still would allow someone to steal delegation from separate tx by replacing repayment address and chain. I think it would be safer and more consistent if we hashed all fill_relay parameters. maybe except for relay_data as that is already represented by relay_hash

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do tests depend on state.seed? if not, we better drop the state seed here as its 0 on public networks

Comment on lines 122 to 124
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]];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider moving seed derivation in transfer_from helper

Comment on lines 20 to 25
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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use AnchorSerialize::serialize or try_to_vec, so that we don't need to be explicit on individual field encoding

md0x added 5 commits April 18, 2025 18:30
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
@md0x md0x marked this pull request as draft April 19, 2025 08:41
md0x and others added 4 commits April 20, 2025 11:15
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
md0x added 2 commits April 20, 2025 11:38
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
@md0x md0x marked this pull request as ready for review April 20, 2025 09:46
@md0x md0x requested a review from Reinis-FRP April 20, 2025 09:47
md0x added 2 commits April 20, 2025 11:49
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants