Skip to content
This repository was archived by the owner on Jun 16, 2025. It is now read-only.

Commit 13f0773

Browse files
authored
solana: safer transfer authority (#153)
Co-authored-by: A5 Pickle <[email protected]>
1 parent 7641539 commit 13f0773

File tree

7 files changed

+132
-59
lines changed

7 files changed

+132
-59
lines changed

.github/workflows/solana.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ jobs:
4343
- name: make lint
4444
run: make lint
4545
working-directory: ./solana
46-
make-anchor-build-idl:
47-
name: make idl
46+
make-check-idl:
47+
name: make check-idl
4848
runs-on: ubuntu-latest
4949
steps:
5050
- uses: actions/checkout@v4
@@ -56,8 +56,8 @@ jobs:
5656
- name: Set default Rust toolchain
5757
run: rustup default stable
5858
working-directory: ./solana
59-
- name: make idl
60-
run: make idl
59+
- name: make check-idl
60+
run: make check-idl
6161
working-directory: ./solana
6262
make-anchor-test:
6363
name: make anchor-test

solana/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ idl:
5656
mkdir -p ts/src/idl/ts
5757
cp -r target/idl/* ts/src/idl/json/
5858
cp -r target/types/* ts/src/idl/ts/
59+
60+
.PHONY: check-idl
61+
check-idl: idl
5962
git diff --exit-code
6063

6164
$(BUILD_$(NETWORK)): cargo-test

solana/programs/token-router/src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ pub enum TokenRouterError {
2626
AlreadyOwner = 0x204,
2727
NoTransferOwnershipRequest = 0x206,
2828
NotPendingOwner = 0x208,
29-
MissingAuthority = 0x20a,
30-
TooManyAuthorities = 0x20b,
29+
EitherSenderOrProgramTransferAuthority = 0x20a,
30+
DelegatedAmountMismatch = 0x20c,
3131

3232
InsufficientAmount = 0x400,
3333
MinAmountOutTooHigh = 0x402,

solana/programs/token-router/src/processor/market_order/prepare.rs

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ pub struct PrepareMarketOrder<'info> {
1818
custodian: CheckedCustodian<'info>,
1919

2020
/// The auction participant needs to set approval to this PDA if the sender (signer) is not
21-
/// provided.
21+
/// provided. The delegated amount must equal the amount in or this instruction will revert.
22+
///
23+
/// NOTE: If this account is provided, the sender token's owner will be encoded as the order
24+
/// sender.
2225
///
2326
/// CHECK: Seeds must be \["transfer-authority", prepared_order.key(), args.hash()\].
2427
#[account(
@@ -28,11 +31,22 @@ pub struct PrepareMarketOrder<'info> {
2831
&args.hash().0,
2932
],
3033
bump,
34+
constraint = {
35+
require_eq!(
36+
sender_token.delegated_amount,
37+
args.amount_in,
38+
TokenRouterError::DelegatedAmountMismatch,
39+
);
40+
41+
true
42+
}
3143
)]
3244
program_transfer_authority: Option<UncheckedAccount<'info>>,
3345

3446
/// Sender, who has the authority to transfer assets from the sender token account. If this
3547
/// account is not provided, the program transfer authority account must be some account.
48+
///
49+
/// NOTE: If this account is provided, this pubkey will be encoded as the order sender.
3650
sender: Option<Signer<'info>>,
3751

3852
#[account(
@@ -161,10 +175,57 @@ pub fn prepare_market_order(
161175
redeemer_message,
162176
} = args;
163177

178+
// Finally transfer amount to custody token account. We perform exclusive or because we do not
179+
// want to allow specifying more than one authority.
180+
let order_sender = match (
181+
ctx.accounts.sender.as_ref(),
182+
ctx.accounts.program_transfer_authority.as_ref(),
183+
) {
184+
(Some(sender), None) => {
185+
token::transfer(
186+
CpiContext::new(
187+
ctx.accounts.token_program.to_account_info(),
188+
token::Transfer {
189+
from: ctx.accounts.sender_token.to_account_info(),
190+
to: ctx.accounts.prepared_custody_token.to_account_info(),
191+
authority: sender.to_account_info(),
192+
},
193+
),
194+
amount_in,
195+
)?;
196+
197+
sender.key()
198+
}
199+
(None, Some(program_transfer_authority)) => {
200+
let sender_token = &ctx.accounts.sender_token;
201+
202+
token::transfer(
203+
CpiContext::new_with_signer(
204+
ctx.accounts.token_program.to_account_info(),
205+
token::Transfer {
206+
from: sender_token.to_account_info(),
207+
to: ctx.accounts.prepared_custody_token.to_account_info(),
208+
authority: program_transfer_authority.to_account_info(),
209+
},
210+
&[&[
211+
TRANSFER_AUTHORITY_SEED_PREFIX,
212+
ctx.accounts.prepared_order.key().as_ref(),
213+
&hashed_args.0,
214+
&[ctx.bumps.program_transfer_authority.unwrap()],
215+
]],
216+
),
217+
amount_in,
218+
)?;
219+
220+
sender_token.owner
221+
}
222+
_ => return err!(TokenRouterError::EitherSenderOrProgramTransferAuthority),
223+
};
224+
164225
// Set the values in prepared order account.
165226
ctx.accounts.prepared_order.set_inner(PreparedOrder {
166227
info: PreparedOrderInfo {
167-
order_sender: ctx.accounts.sender_token.owner,
228+
order_sender,
168229
prepared_by: ctx.accounts.payer.key(),
169230
order_type: OrderType::Market { min_amount_out },
170231
src_token: ctx.accounts.sender_token.key(),
@@ -176,41 +237,6 @@ pub fn prepare_market_order(
176237
redeemer_message,
177238
});
178239

179-
// Finally transfer amount to custody token account. We perform exclusive or because we do not
180-
// want to allow specifying more than one authority.
181-
match (
182-
ctx.accounts.sender.as_ref(),
183-
ctx.accounts.program_transfer_authority.as_ref(),
184-
) {
185-
(Some(sender), None) => token::transfer(
186-
CpiContext::new(
187-
ctx.accounts.token_program.to_account_info(),
188-
token::Transfer {
189-
from: ctx.accounts.sender_token.to_account_info(),
190-
to: ctx.accounts.prepared_custody_token.to_account_info(),
191-
authority: sender.to_account_info(),
192-
},
193-
),
194-
amount_in,
195-
),
196-
(None, Some(program_transfer_authority)) => token::transfer(
197-
CpiContext::new_with_signer(
198-
ctx.accounts.token_program.to_account_info(),
199-
token::Transfer {
200-
from: ctx.accounts.sender_token.to_account_info(),
201-
to: ctx.accounts.prepared_custody_token.to_account_info(),
202-
authority: program_transfer_authority.to_account_info(),
203-
},
204-
&[&[
205-
TRANSFER_AUTHORITY_SEED_PREFIX,
206-
ctx.accounts.prepared_order.key().as_ref(),
207-
&hashed_args.0,
208-
&[ctx.bumps.program_transfer_authority.unwrap()],
209-
]],
210-
),
211-
amount_in,
212-
),
213-
(None, None) => err!(TokenRouterError::MissingAuthority),
214-
(Some(_), Some(_)) => err!(TokenRouterError::TooManyAuthorities),
215-
}
240+
// Done.
241+
Ok(())
216242
}

solana/ts/src/idl/json/token_router.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,10 @@
525525
"name": "program_transfer_authority",
526526
"docs": [
527527
"The auction participant needs to set approval to this PDA if the sender (signer) is not",
528-
"provided.",
528+
"provided. The delegated amount must equal the amount in or this instruction will revert.",
529+
"",
530+
"NOTE: If this account is provided, the sender token's owner will be encoded as the order",
531+
"sender.",
529532
""
530533
],
531534
"optional": true
@@ -534,7 +537,9 @@
534537
"name": "sender",
535538
"docs": [
536539
"Sender, who has the authority to transfer assets from the sender token account. If this",
537-
"account is not provided, the program transfer authority account must be some account."
540+
"account is not provided, the program transfer authority account must be some account.",
541+
"",
542+
"NOTE: If this account is provided, this pubkey will be encoded as the order sender."
538543
],
539544
"signer": true,
540545
"optional": true
@@ -1122,11 +1127,11 @@
11221127
},
11231128
{
11241129
"code": 6522,
1125-
"name": "MissingAuthority"
1130+
"name": "EitherSenderOrProgramTransferAuthority"
11261131
},
11271132
{
1128-
"code": 6523,
1129-
"name": "TooManyAuthorities"
1133+
"code": 6524,
1134+
"name": "DelegatedAmountMismatch"
11301135
},
11311136
{
11321137
"code": 7024,

solana/ts/src/idl/ts/token_router.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,10 @@ export type TokenRouter = {
531531
"name": "programTransferAuthority",
532532
"docs": [
533533
"The auction participant needs to set approval to this PDA if the sender (signer) is not",
534-
"provided.",
534+
"provided. The delegated amount must equal the amount in or this instruction will revert.",
535+
"",
536+
"NOTE: If this account is provided, the sender token's owner will be encoded as the order",
537+
"sender.",
535538
""
536539
],
537540
"optional": true
@@ -540,7 +543,9 @@ export type TokenRouter = {
540543
"name": "sender",
541544
"docs": [
542545
"Sender, who has the authority to transfer assets from the sender token account. If this",
543-
"account is not provided, the program transfer authority account must be some account."
546+
"account is not provided, the program transfer authority account must be some account.",
547+
"",
548+
"NOTE: If this account is provided, this pubkey will be encoded as the order sender."
544549
],
545550
"signer": true,
546551
"optional": true
@@ -1128,11 +1133,11 @@ export type TokenRouter = {
11281133
},
11291134
{
11301135
"code": 6522,
1131-
"name": "missingAuthority"
1136+
"name": "eitherSenderOrProgramTransferAuthority"
11321137
},
11331138
{
1134-
"code": 6523,
1135-
"name": "tooManyAuthorities"
1139+
"code": 6524,
1140+
"name": "delegatedAmountMismatch"
11361141
},
11371142
{
11381143
"code": 7024,

solana/ts/tests/02__tokenRouter.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,41 @@ describe("Token Router", function () {
543543
connection,
544544
[ix],
545545
[payer, preparedOrder],
546-
"Error: owner does not match",
546+
"Error Code: DelegatedAmountMismatch",
547+
);
548+
});
549+
550+
it("Cannot Prepare Market Order Delegating Too Much to Program Transfer Authority", async function () {
551+
const preparedOrder = Keypair.generate();
552+
553+
const amountIn = 69n;
554+
const minAmountOut = 0n;
555+
const targetChain = foreignChain;
556+
const redeemer = Array.from(Buffer.alloc(32, "deadbeef", "hex"));
557+
const redeemerMessage = Buffer.from("All your base are belong to us");
558+
const [approveIx, ix] = await tokenRouter.prepareMarketOrderIx(
559+
{
560+
payer: payer.publicKey,
561+
preparedOrder: preparedOrder.publicKey,
562+
senderToken: payerToken,
563+
},
564+
{
565+
amountIn,
566+
minAmountOut,
567+
targetChain,
568+
redeemer,
569+
redeemerMessage,
570+
},
571+
);
572+
573+
// Approve for more.
574+
approveIx!.data.writeBigUInt64LE(amountIn + 1n, 1);
575+
576+
await expectIxErr(
577+
connection,
578+
[approveIx!, ix],
579+
[payer, preparedOrder],
580+
"Error Code: DelegatedAmountMismatch",
547581
);
548582
});
549583

@@ -575,9 +609,9 @@ describe("Token Router", function () {
575609

576610
await expectIxErr(
577611
connection,
578-
[ix],
612+
[approveIx!, ix],
579613
[payer, preparedOrder],
580-
"Error Code: TooManyAuthorities",
614+
"Error Code: EitherSenderOrProgramTransferAuthority",
581615
);
582616
});
583617

@@ -610,7 +644,7 @@ describe("Token Router", function () {
610644
connection,
611645
[ix],
612646
[payer, preparedOrder],
613-
"Error Code: MissingAuthority",
647+
"Error Code: EitherSenderOrProgramTransferAuthority",
614648
);
615649
});
616650

0 commit comments

Comments
 (0)