Skip to content

Commit 5fe9e35

Browse files
authored
fix: delegated transfers in deposits (#752)
* fix: delegated transfers in deposits Signed-off-by: Reinis Martinsons <[email protected]> * fix: delegate in deposit scripts Signed-off-by: Reinis Martinsons <[email protected]> * fix: delegated transfers in fills Signed-off-by: Reinis Martinsons <[email protected]> * fix: update slow fill test Signed-off-by: Reinis Martinsons <[email protected]> * fix: update fill script Signed-off-by: Reinis Martinsons <[email protected]> * test: unapproved deposits Signed-off-by: Reinis Martinsons <[email protected]> --------- Signed-off-by: Reinis Martinsons <[email protected]>
1 parent 7f9ebb3 commit 5fe9e35

File tree

13 files changed

+565
-275
lines changed

13 files changed

+565
-275
lines changed

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use anchor_lang::prelude::*;
2-
use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked};
2+
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
33

44
use crate::{
55
error::{CommonError, SvmError},
66
event::V3FundsDeposited,
77
get_current_time,
88
state::{Route, State},
9+
utils::transfer_from,
910
};
1011

1112
#[event_cpi]
@@ -89,14 +90,16 @@ pub fn deposit_v3(
8990
return err!(CommonError::InvalidFillDeadline);
9091
}
9192

92-
let transfer_accounts = TransferChecked {
93-
from: ctx.accounts.depositor_token_account.to_account_info(),
94-
mint: ctx.accounts.mint.to_account_info(),
95-
to: ctx.accounts.vault.to_account_info(),
96-
authority: ctx.accounts.signer.to_account_info(),
97-
};
98-
let cpi_context = CpiContext::new(ctx.accounts.token_program.to_account_info(), transfer_accounts);
99-
transfer_checked(cpi_context, input_amount, ctx.accounts.mint.decimals)?;
93+
// Depositor must have delegated input_amount to the state PDA.
94+
transfer_from(
95+
&ctx.accounts.depositor_token_account,
96+
&ctx.accounts.vault,
97+
input_amount,
98+
state,
99+
ctx.bumps.state,
100+
&ctx.accounts.mint,
101+
&ctx.accounts.token_program,
102+
)?;
100103

101104
state.number_of_deposits += 1; // Increment number of deposits
102105

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

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anchor_lang::prelude::*;
22
use anchor_spl::{
33
associated_token::AssociatedToken,
4-
token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked},
4+
token_interface::{Mint, TokenAccount, TokenInterface},
55
};
66

77
use crate::{
@@ -12,6 +12,7 @@ use crate::{
1212
event::{FillType, FilledV3Relay, V3RelayExecutionEventInfo},
1313
get_current_time,
1414
state::{FillStatus, FillStatusAccount, State},
15+
utils::transfer_from,
1516
};
1617

1718
#[event_cpi]
@@ -101,17 +102,15 @@ pub fn fill_v3_relay(
101102
// If relayer and receiver are the same, there is no need to do the transfer. This might be a case when relayers
102103
// intentionally self-relay in a capital efficient way (no need to have funds on the destination).
103104
if ctx.accounts.relayer_token_account.key() != ctx.accounts.recipient_token_account.key() {
104-
let transfer_accounts = TransferChecked {
105-
from: ctx.accounts.relayer_token_account.to_account_info(),
106-
mint: ctx.accounts.mint_account.to_account_info(),
107-
to: ctx.accounts.recipient_token_account.to_account_info(),
108-
authority: ctx.accounts.signer.to_account_info(),
109-
};
110-
let cpi_context = CpiContext::new(ctx.accounts.token_program.to_account_info(), transfer_accounts);
111-
transfer_checked(
112-
cpi_context,
105+
// Relayer must have delegated output_amount to the state PDA (but only if not self-relaying)
106+
transfer_from(
107+
&ctx.accounts.relayer_token_account,
108+
&ctx.accounts.recipient_token_account,
113109
relay_data.output_amount,
114-
ctx.accounts.mint_account.decimals,
110+
state,
111+
ctx.bumps.state,
112+
&ctx.accounts.mint_account,
113+
&ctx.accounts.token_program,
115114
)?;
116115
}
117116

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
pub mod bitmap_utils;
22
pub mod cctp_utils;
33
pub mod merkle_proof_utils;
4+
pub mod transfer_utils;
45

56
pub use bitmap_utils::*;
67
pub use cctp_utils::*;
78
pub use merkle_proof_utils::*;
9+
pub use transfer_utils::*;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked};
3+
4+
use crate::State;
5+
6+
pub fn transfer_from<'info>(
7+
from: &InterfaceAccount<'info, TokenAccount>,
8+
to: &InterfaceAccount<'info, TokenAccount>,
9+
amount: u64,
10+
state: &Account<'info, State>,
11+
state_bump: u8,
12+
mint: &InterfaceAccount<'info, Mint>,
13+
token_program: &Interface<'info, TokenInterface>,
14+
) -> Result<()> {
15+
let transfer_accounts = TransferChecked {
16+
from: from.to_account_info(),
17+
mint: mint.to_account_info(),
18+
to: to.to_account_info(),
19+
authority: state.to_account_info(),
20+
};
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+
26+
let cpi_context = CpiContext::new_with_signer(token_program.to_account_info(), transfer_accounts, signer_seeds);
27+
28+
transfer_checked(cpi_context, amount, mint.decimals)
29+
}

scripts/svm/simpleDeposit.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
import * as anchor from "@coral-xyz/anchor";
44
import { BN, Program, AnchorProvider } from "@coral-xyz/anchor";
5-
import { PublicKey } from "@solana/web3.js";
6-
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from "@solana/spl-token";
5+
import { PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
6+
import {
7+
ASSOCIATED_TOKEN_PROGRAM_ID,
8+
TOKEN_PROGRAM_ID,
9+
createApproveCheckedInstruction,
10+
getAssociatedTokenAddressSync,
11+
getMint,
12+
} from "@solana/spl-token";
713
import { SvmSpoke } from "../../target/types/svm_spoke";
814
import yargs from "yargs";
915
import { hideBin } from "yargs/helpers";
@@ -53,7 +59,7 @@ async function depositV3(): Promise<void> {
5359
);
5460

5561
// Define the signer (replace with your actual signer)
56-
const signer = provider.wallet.publicKey;
62+
const signer = (provider.wallet as anchor.Wallet).payer;
5763

5864
// Find ATA for the input token to be stored by state (vault). This was created when the route was enabled.
5965
const vault = getAssociatedTokenAddressSync(
@@ -83,9 +89,25 @@ async function depositV3(): Promise<void> {
8389
{ property: "vault", value: vault.toString() },
8490
]);
8591

86-
const tx = await (
92+
const userTokenAccount = getAssociatedTokenAddressSync(inputToken, signer.publicKey);
93+
94+
const tokenDecimals = (await getMint(provider.connection, inputToken, undefined, TOKEN_PROGRAM_ID)).decimals;
95+
96+
// Delegate state PDA to pull depositor tokens.
97+
const approveIx = await createApproveCheckedInstruction(
98+
userTokenAccount,
99+
inputToken,
100+
statePda,
101+
signer.publicKey,
102+
BigInt(inputAmount.toString()),
103+
tokenDecimals,
104+
undefined,
105+
TOKEN_PROGRAM_ID
106+
);
107+
108+
const depositIx = await (
87109
program.methods.depositV3(
88-
signer,
110+
signer.publicKey,
89111
recipient,
90112
inputToken,
91113
outputToken,
@@ -102,13 +124,15 @@ async function depositV3(): Promise<void> {
102124
.accounts({
103125
state: statePda,
104126
route: routePda,
105-
signer: signer,
106-
userTokenAccount: getAssociatedTokenAddressSync(inputToken, signer),
127+
signer: signer.publicKey,
128+
userTokenAccount,
107129
vault: vault,
108130
tokenProgram: TOKEN_PROGRAM_ID,
109131
mint: inputToken,
110132
})
111-
.rpc();
133+
.instruction();
134+
const depositTx = new Transaction().add(approveIx, depositIx);
135+
const tx = await sendAndConfirmTransaction(provider.connection, depositTx, [signer]);
112136

113137
console.log("Transaction signature:", tx);
114138
}

scripts/svm/simpleFakeRelayerRepayment.ts

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ import {
1010
AddressLookupTableProgram,
1111
VersionedTransaction,
1212
TransactionMessage,
13+
sendAndConfirmTransaction,
14+
Transaction,
1315
} from "@solana/web3.js";
1416
import {
1517
ASSOCIATED_TOKEN_PROGRAM_ID,
1618
TOKEN_PROGRAM_ID,
1719
getAssociatedTokenAddressSync,
1820
createAssociatedTokenAccount,
21+
getMint,
22+
createApproveCheckedInstruction,
1923
} from "@solana/spl-token";
2024
import { SvmSpoke } from "../../target/types/svm_spoke";
2125
import yargs from "yargs";
@@ -49,8 +53,8 @@ async function testBundleLogic(): Promise<void> {
4953
const amounts = Array.from({ length: numberOfRelayersToRepay }, (_, i) => new BN(i + 1));
5054
const inputToken = new PublicKey(resolvedArgv.inputToken);
5155

52-
const signer = provider.wallet.publicKey;
53-
console.log("Running from signer: ", signer.toString());
56+
const signer = (provider.wallet as anchor.Wallet).payer;
57+
console.log("Running from signer: ", signer.publicKey.toString());
5458

5559
const [statePda, _] = PublicKey.findProgramAddressSync(
5660
[Buffer.from("state"), seed.toArrayLike(Buffer, "le", 8)],
@@ -76,21 +80,38 @@ async function testBundleLogic(): Promise<void> {
7680
{ property: "seed", value: seed.toString() },
7781
{ property: "numberOfRelayersToRepay", value: numberOfRelayersToRepay },
7882
{ property: "inputToken", value: inputToken.toString() },
79-
{ property: "signer", value: signer.toString() },
83+
{ property: "signer", value: signer.publicKey.toString() },
8084
{ property: "statePda", value: statePda.toString() },
8185
{ property: "routePda", value: routePda.toString() },
8286
{ property: "vault", value: vault.toString() },
8387
]);
8488

89+
const userTokenAccount = getAssociatedTokenAddressSync(inputToken, signer.publicKey);
90+
91+
const tokenDecimals = (await getMint(provider.connection, inputToken, undefined, TOKEN_PROGRAM_ID)).decimals;
92+
8593
// Use program.methods.depositV3 to send tokens to the spoke. note this is NOT a valid deposit, we just want to
8694
// seed tokens into the spoke to test repayment.
87-
const depositTx = await (
95+
96+
// Delegate state PDA to pull depositor tokens.
97+
const inputAmount = amounts.reduce((acc, amount) => acc.add(amount), new BN(0));
98+
const approveIx = await createApproveCheckedInstruction(
99+
userTokenAccount,
100+
inputToken,
101+
statePda,
102+
signer.publicKey,
103+
BigInt(inputAmount.toString()),
104+
tokenDecimals,
105+
undefined,
106+
TOKEN_PROGRAM_ID
107+
);
108+
const depositIx = await (
88109
program.methods.depositV3(
89-
signer,
90-
signer, // recipient is the signer for this example
110+
signer.publicKey,
111+
signer.publicKey, // recipient is the signer for this example
91112
inputToken,
92113
inputToken, // Re-use inputToken as outputToken. does not matter for this deposit.
93-
amounts.reduce((acc, amount) => acc.add(amount), new BN(0)),
114+
inputAmount,
94115
new BN(0),
95116
new BN(11155111), // destinationChainId. assumed to be enabled, as with routePDA
96117
PublicKey.default, // exclusiveRelayer
@@ -103,13 +124,17 @@ async function testBundleLogic(): Promise<void> {
103124
.accounts({
104125
state: statePda,
105126
route: routePda,
106-
signer: signer,
107-
userTokenAccount: getAssociatedTokenAddressSync(inputToken, signer),
127+
signer: signer.publicKey,
128+
userTokenAccount: getAssociatedTokenAddressSync(inputToken, signer.publicKey),
108129
vault: vault,
109130
tokenProgram: TOKEN_PROGRAM_ID,
110131
mint: inputToken,
111132
})
112-
.rpc();
133+
.instruction();
134+
const depositTx = await sendAndConfirmTransaction(provider.connection, new Transaction().add(approveIx, depositIx), [
135+
signer,
136+
]);
137+
113138
console.log(`Deposit transaction sent: ${depositTx}`);
114139

115140
// Create a single repayment leaf with the array of amounts and corresponding refund addresses
@@ -161,15 +186,15 @@ async function testBundleLogic(): Promise<void> {
161186
{ property: "State PDA", value: statePda.toString() },
162187
{ property: "Route PDA", value: routePda.toString() },
163188
{ property: "Root Bundle PDA", value: rootBundle.toString() },
164-
{ property: "Signer", value: signer.toString() },
189+
{ property: "Signer", value: signer.publicKey.toString() },
165190
]);
166191

167192
const relayRootBundleTx = await (program.methods.relayRootBundle(Array.from(root), Array.from(root)) as any)
168193
.accounts({
169194
state: statePda,
170195
rootBundle: rootBundle,
171-
signer: signer,
172-
payer: signer,
196+
signer: signer.publicKey,
197+
payer: signer.publicKey,
173198
systemProgram: SystemProgram.programId,
174199
})
175200
.rpc();
@@ -190,15 +215,15 @@ async function testBundleLogic(): Promise<void> {
190215
console.log("loading execute relayer refund leaf params...");
191216

192217
const [instructionParams] = PublicKey.findProgramAddressSync(
193-
[Buffer.from("instruction_params"), signer.toBuffer()],
218+
[Buffer.from("instruction_params"), signer.publicKey.toBuffer()],
194219
program.programId
195220
);
196221

197222
const staticAccounts = {
198223
instructionParams,
199224
state: statePda,
200225
rootBundle: rootBundle,
201-
signer: signer,
226+
signer: signer.publicKey,
202227
vault: vault,
203228
tokenProgram: TOKEN_PROGRAM_ID,
204229
mint: inputToken,
@@ -213,8 +238,8 @@ async function testBundleLogic(): Promise<void> {
213238

214239
// Consolidate all above addresses into a single array for the Address Lookup Table (ALT).
215240
const [lookupTableInstruction, lookupTableAddress] = await AddressLookupTableProgram.createLookupTable({
216-
authority: signer,
217-
payer: signer,
241+
authority: signer.publicKey,
242+
payer: signer.publicKey,
218243
recentSlot: await provider.connection.getSlot(),
219244
});
220245

@@ -235,8 +260,8 @@ async function testBundleLogic(): Promise<void> {
235260
for (let i = 0; i < lookupAddresses.length; i += maxExtendedAccounts) {
236261
const extendInstruction = AddressLookupTableProgram.extendLookupTable({
237262
lookupTable: lookupTableAddress,
238-
authority: signer,
239-
payer: signer,
263+
authority: signer.publicKey,
264+
payer: signer.publicKey,
240265
addresses: lookupAddresses.slice(i, i + maxExtendedAccounts),
241266
});
242267

@@ -253,7 +278,7 @@ async function testBundleLogic(): Promise<void> {
253278
throw new Error("AddressLookupTableAccount not fetched");
254279
}
255280

256-
await loadExecuteRelayerRefundLeafParams(program, signer, rootBundleId, leaf, proofAsNumbers);
281+
await loadExecuteRelayerRefundLeafParams(program, signer.publicKey, rootBundleId, leaf, proofAsNumbers);
257282

258283
console.log(`loaded execute relayer refund leaf params ${instructionParams}. \nExecuting relayer refund leaf...`);
259284

@@ -267,7 +292,7 @@ async function testBundleLogic(): Promise<void> {
267292
const computeBudgetInstruction = ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 });
268293
const versionedTx = new VersionedTransaction(
269294
new TransactionMessage({
270-
payerKey: signer,
295+
payerKey: signer.publicKey,
271296
recentBlockhash: (await provider.connection.getLatestBlockhash()).blockhash,
272297
instructions: [computeBudgetInstruction, executeInstruction],
273298
}).compileToV0Message([lookupTableAccount])
@@ -280,8 +305,9 @@ async function testBundleLogic(): Promise<void> {
280305

281306
// Close the instruction parameters account
282307
console.log("Closing instruction params...");
308+
await new Promise((resolve) => setTimeout(resolve, 15000)); // Wait for the previous transaction to be processed.
283309
const closeInstructionParamsTx = await (program.methods.closeInstructionParams() as any)
284-
.accounts({ signer: signer, instructionParams: instructionParams })
310+
.accounts({ signer: signer.publicKey, instructionParams: instructionParams })
285311
.rpc();
286312
console.log(`Close instruction params transaction sent: ${closeInstructionParamsTx}`);
287313
// Note we cant close the lookup table account as it needs to be both deactivated and expired at to do this.

0 commit comments

Comments
 (0)