Skip to content

Commit 6f7fde4

Browse files
authored
solana: introduce "session authority" to remove atomic approval requirement (#304)
* solana: introduce "session authority" to remove atomic approval requirement * solana: remove sender, just compute from the token account owner
1 parent 30dcba2 commit 6f7fde4

File tree

10 files changed

+230
-118
lines changed

10 files changed

+230
-118
lines changed

solana/.eslintrc.json

+10-4
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@
99
"ecmaVersion": "latest",
1010
"sourceType": "module"
1111
},
12-
plugins: [
13-
'@stylistic'
12+
"plugins": [
13+
"@stylistic"
1414
],
1515
"rules": {
16-
'@stylistic/indent': ['error', 2],
17-
'@stylistic/max-len': ['error', 80],
16+
"@stylistic/indent": [
17+
"error",
18+
2
19+
],
20+
"@stylistic/max-len": [
21+
"error",
22+
80
23+
]
1824
}
1925
}

solana/package-lock.json

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

solana/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
},
88
"dependencies": {
99
"@certusone/wormhole-sdk": "^0.10.10",
10-
"@wormhole-foundation/sdk-base": "^0.5.0-beta.9",
11-
"@wormhole-foundation/sdk-definitions": "^0.5.0-beta.9",
1210
"@coral-xyz/anchor": "^0.29.0",
1311
"@solana/spl-token": "^0.4.0",
1412
"@solana/web3.js": "^1.90.0",
13+
"@wormhole-foundation/sdk-base": "^0.5.0-beta.9",
14+
"@wormhole-foundation/sdk-definitions": "^0.5.0-beta.9",
1515
"dotenv": "^16.4.5",
1616
"sha3": "^2.1.4"
1717
},
@@ -32,6 +32,6 @@
3232
"mocha": "^9.0.3",
3333
"prettier": "^2.6.2",
3434
"ts-mocha": "^10.0.0",
35-
"typescript": "^4.9.5"
35+
"typescript": "^5.4.2"
3636
}
3737
}

solana/programs/example-native-token-transfers/src/instructions/transfer.rs

+50-21
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ pub struct Transfer<'info> {
2424

2525
pub config: NotPausedConfig<'info>,
2626

27-
/// This signer will be encoded in the outbox.
28-
sender: Signer<'info>,
29-
3027
#[account(
3128
mut,
3229
address = config.mint,
@@ -38,6 +35,8 @@ pub struct Transfer<'info> {
3835
mut,
3936
token::mint = mint,
4037
)]
38+
/// CHECK: the spl token program will check that the session_authority
39+
/// account can spend these tokens.
4140
pub from: InterfaceAccount<'info, token_interface::TokenAccount>,
4241

4342
pub token_program: Interface<'info, token_interface::TokenInterface>,
@@ -52,14 +51,6 @@ pub struct Transfer<'info> {
5251
#[account(mut)]
5352
pub outbox_rate_limit: Account<'info, OutboxRateLimit>,
5453

55-
/// CHECK: This authority will need to have been delegated authority to
56-
/// transfer or burn tokens in the [from](Self::from) account.
57-
#[account(
58-
seeds = [crate::TOKEN_AUTHORITY_SEED],
59-
bump,
60-
)]
61-
token_authority: AccountInfo<'info>,
62-
6354
pub system_program: Program<'info, System>,
6455
}
6556

@@ -71,6 +62,23 @@ pub struct TransferArgs {
7162
pub should_queue: bool,
7263
}
7364

65+
impl TransferArgs {
66+
pub fn keccak256(&self) -> solana_program::keccak::Hash {
67+
let TransferArgs {
68+
amount,
69+
recipient_chain,
70+
recipient_address,
71+
should_queue,
72+
} = self;
73+
solana_program::keccak::hashv(&[
74+
amount.to_be_bytes().as_ref(),
75+
recipient_chain.id.to_be_bytes().as_ref(),
76+
recipient_address,
77+
&[*should_queue as u8],
78+
])
79+
}
80+
}
81+
7482
// Burn/mint
7583

7684
#[derive(Accounts)]
@@ -92,9 +100,18 @@ pub struct TransferBurn<'info> {
92100
bump = peer.bump,
93101
)]
94102
pub peer: Account<'info, NttManagerPeer>,
103+
104+
#[account(
105+
seeds = [
106+
crate::SESSION_AUTHORITY_SEED,
107+
common.from.owner.as_ref(),
108+
args.keccak256().as_ref()
109+
],
110+
bump,
111+
)]
112+
pub session_authority: AccountInfo<'info>,
95113
}
96114

97-
// TODO: fees for relaying?
98115
pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<()> {
99116
require_eq!(
100117
ctx.accounts.common.config.mode,
@@ -123,11 +140,13 @@ pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<(
123140
token_interface::Burn {
124141
mint: accs.common.mint.to_account_info(),
125142
from: accs.common.from.to_account_info(),
126-
authority: accs.common.token_authority.to_account_info(),
143+
authority: accs.session_authority.to_account_info(),
127144
},
128145
&[&[
129-
crate::TOKEN_AUTHORITY_SEED,
130-
&[ctx.bumps.common.token_authority],
146+
crate::SESSION_AUTHORITY_SEED,
147+
accs.common.from.owner.as_ref(),
148+
args.keccak256().as_ref(),
149+
&[ctx.bumps.session_authority],
131150
]],
132151
),
133152
amount,
@@ -169,15 +188,23 @@ pub struct TransferLock<'info> {
169188
)]
170189
pub peer: Account<'info, NttManagerPeer>,
171190

191+
#[account(
192+
seeds = [
193+
crate::SESSION_AUTHORITY_SEED,
194+
common.from.owner.as_ref(),
195+
args.keccak256().as_ref()
196+
],
197+
bump,
198+
)]
199+
pub session_authority: AccountInfo<'info>,
200+
172201
#[account(
173202
mut,
174203
address = common.config.custody
175204
)]
176205
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>,
177206
}
178207

179-
// TODO: fees for relaying?
180-
// TODO: factor out common bits
181208
pub fn transfer_lock(ctx: Context<TransferLock>, args: TransferArgs) -> Result<()> {
182209
require_eq!(
183210
ctx.accounts.common.config.mode,
@@ -206,12 +233,14 @@ pub fn transfer_lock(ctx: Context<TransferLock>, args: TransferArgs) -> Result<(
206233
token_interface::TransferChecked {
207234
from: accs.common.from.to_account_info(),
208235
to: accs.custody.to_account_info(),
209-
authority: accs.common.token_authority.to_account_info(),
236+
authority: accs.session_authority.to_account_info(),
210237
mint: accs.common.mint.to_account_info(),
211238
},
212239
&[&[
213-
crate::TOKEN_AUTHORITY_SEED,
214-
&[ctx.bumps.common.token_authority],
240+
crate::SESSION_AUTHORITY_SEED,
241+
accs.common.from.owner.as_ref(),
242+
args.keccak256().as_ref(),
243+
&[ctx.bumps.session_authority],
215244
]],
216245
),
217246
amount,
@@ -260,7 +289,7 @@ fn insert_into_outbox(
260289

261290
common.outbox_item.set_inner(OutboxItem {
262291
amount: trimmed_amount,
263-
sender: common.sender.key(),
292+
sender: common.from.owner,
264293
recipient_chain,
265294
recipient_ntt_manager,
266295
recipient_address,

solana/programs/example-native-token-transfers/src/lib.rs

+25-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,31 @@ cfg_if::cfg_if! {
3636
}
3737
}
3838

39-
const TOKEN_AUTHORITY_SEED: &[u8] = b"token_authority";
39+
pub const TOKEN_AUTHORITY_SEED: &[u8] = b"token_authority";
40+
41+
/// The seed for the session authority account.
42+
///
43+
/// These accounts are used in the `transfer_*` instructions. The user first
44+
/// approves the session authority to spend the tokens, and then the session
45+
/// authority burns or locks the tokens.
46+
/// This is to avoid the user having to pass their own authority to the program,
47+
/// which in general is dangerous, especially for upgradeable programs.
48+
///
49+
/// There is a session authority associated with each transfer, and is seeded by
50+
/// the sender's pubkey, and (the hash of) all the transfer arguments.
51+
/// These seeds essentially encode the user's intent when approving the
52+
/// spending.
53+
///
54+
/// In practice, the approve instruction is going to be atomically bundled with
55+
/// the transfer instruction, so this encoding makes no difference.
56+
/// However, it does allow it to be done in a separate transaction without the
57+
/// risk of a malicious actor redirecting the funds by frontrunning the transfer
58+
/// instruction.
59+
/// In other words, the transfer instruction has no degrees of freedom; all the
60+
/// arguments are determined in the approval step. Then transfer can be
61+
/// permissionlessly invoked by anyone (even if in practice it's going to be the
62+
/// user, atomically).
63+
pub const SESSION_AUTHORITY_SEED: &[u8] = b"session_authority";
4064

4165
#[program]
4266
pub mod example_native_token_transfers {

solana/programs/example-native-token-transfers/tests/cancel_flow.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,13 @@ async fn test_cancel() {
219219
&test_data.ntt,
220220
&test_data.user_token_account,
221221
&test_data.user.pubkey(),
222-
args.amount,
222+
&args,
223223
)
224224
.submit_with_signers(&[&test_data.user], &mut ctx)
225225
.await
226226
.unwrap();
227227
transfer(&test_data.ntt, accs, args, Mode::Locking)
228-
.submit_with_signers(&[&test_data.user, &outbox_item], &mut ctx)
228+
.submit_with_signers(&[&outbox_item], &mut ctx)
229229
.await
230230
.unwrap();
231231

solana/programs/example-native-token-transfers/tests/sdk/accounts.rs

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use anchor_lang::prelude::Pubkey;
22
use example_native_token_transfers::{
33
config::Config,
4+
instructions::TransferArgs,
45
queue::{
56
inbox::{InboxItem, InboxRateLimit},
67
outbox::OutboxRateLimit,
78
},
89
registered_transceiver::RegisteredTransceiver,
10+
SESSION_AUTHORITY_SEED, TOKEN_AUTHORITY_SEED,
911
};
1012
use ntt_messages::{ntt::NativeTokenTransfer, ntt_manager::NttManagerMessage};
1113
use sha3::{Digest, Keccak256};
@@ -89,6 +91,31 @@ impl NTT {
8991
inbox_rate_limit
9092
}
9193

94+
pub fn session_authority(&self, sender: &Pubkey, args: &TransferArgs) -> Pubkey {
95+
let TransferArgs {
96+
amount,
97+
recipient_chain,
98+
recipient_address,
99+
should_queue,
100+
} = args;
101+
let mut hasher = Keccak256::new();
102+
103+
hasher.update(&amount.to_be_bytes());
104+
hasher.update(&recipient_chain.id.to_be_bytes());
105+
hasher.update(&recipient_address);
106+
hasher.update(&[*should_queue as u8]);
107+
108+
let (session_authority, _) = Pubkey::find_program_address(
109+
&[
110+
SESSION_AUTHORITY_SEED.as_ref(),
111+
sender.as_ref(),
112+
&hasher.finalize(),
113+
],
114+
&self.program,
115+
);
116+
session_authority
117+
}
118+
92119
pub fn inbox_item(
93120
&self,
94121
chain: u16,
@@ -107,7 +134,7 @@ impl NTT {
107134

108135
pub fn token_authority(&self) -> Pubkey {
109136
let (token_authority, _) =
110-
Pubkey::find_program_address(&[b"token_authority".as_ref()], &self.program);
137+
Pubkey::find_program_address(&[TOKEN_AUTHORITY_SEED.as_ref()], &self.program);
111138
token_authority
112139
}
113140

solana/programs/example-native-token-transfers/tests/sdk/instructions/transfer.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ pub fn transfer(ntt: &NTT, transfer: Transfer, args: TransferArgs, mode: Mode) -
2525

2626
pub fn transfer_burn(ntt: &NTT, transfer: Transfer, args: TransferArgs) -> Instruction {
2727
let chain_id = args.recipient_chain.id;
28+
let session_authority = ntt.session_authority(&transfer.from_authority, &args);
2829
let data = example_native_token_transfers::instruction::TransferBurn { args };
2930

3031
let accounts = example_native_token_transfers::accounts::TransferBurn {
3132
common: common(ntt, &transfer),
3233
inbox_rate_limit: ntt.inbox_rate_limit(chain_id),
3334
peer: transfer.peer,
35+
session_authority,
3436
};
3537

3638
Instruction {
@@ -42,13 +44,15 @@ pub fn transfer_burn(ntt: &NTT, transfer: Transfer, args: TransferArgs) -> Instr
4244

4345
pub fn transfer_lock(ntt: &NTT, transfer: Transfer, args: TransferArgs) -> Instruction {
4446
let chain_id = args.recipient_chain.id;
47+
let session_authority = ntt.session_authority(&transfer.from_authority, &args);
4548
let data = example_native_token_transfers::instruction::TransferLock { args };
4649

4750
let accounts = example_native_token_transfers::accounts::TransferLock {
4851
common: common(ntt, &transfer),
4952
inbox_rate_limit: ntt.inbox_rate_limit(chain_id),
5053
peer: transfer.peer,
5154
custody: ntt.custody(&transfer.mint),
55+
session_authority,
5256
};
5357
Instruction {
5458
program_id: example_native_token_transfers::ID,
@@ -61,15 +65,15 @@ pub fn approve_token_authority(
6165
ntt: &NTT,
6266
user_token_account: &Pubkey,
6367
user: &Pubkey,
64-
amount: u64,
68+
args: &TransferArgs,
6569
) -> Instruction {
6670
spl_token_2022::instruction::approve(
6771
&spl_token::id(), // TODO: look into how token account was originally created
6872
user_token_account,
69-
&ntt.token_authority(),
73+
&ntt.session_authority(user, args),
7074
user,
7175
&[user],
72-
amount,
76+
args.amount,
7377
)
7478
.unwrap()
7579
}
@@ -80,13 +84,11 @@ fn common(ntt: &NTT, transfer: &Transfer) -> example_native_token_transfers::acc
8084
config: NotPausedConfig {
8185
config: ntt.config(),
8286
},
83-
sender: transfer.from_authority,
8487
mint: transfer.mint,
8588
from: transfer.from,
8689
token_program: Token::id(),
8790
outbox_item: transfer.outbox_item,
8891
outbox_rate_limit: ntt.outbox_rate_limit(),
89-
token_authority: ntt.token_authority(),
9092
system_program: System::id(),
9193
}
9294
}

0 commit comments

Comments
 (0)