Skip to content

Commit 637c03e

Browse files
committed
wip: transfer before burn/mint to trigger hooks
wip because with this the test tx size is 5 bytes larger (for transfer) than the limit. The way to mitigate is to introduce lookup tables. That mitigation will exist in the tests, but will also have to be done on real deployments (since the transfer hooks might add an arbitrary number of extra acconuts, so we should leave as much headroom as possible)
1 parent 4b55b84 commit 637c03e

File tree

6 files changed

+208
-127
lines changed

6 files changed

+208
-127
lines changed

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

+59-46
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ pub struct ReleaseInbound<'info> {
4141
pub mint: InterfaceAccount<'info, token_interface::Mint>,
4242

4343
pub token_program: Interface<'info, token_interface::TokenInterface>,
44+
45+
/// CHECK: the token program checks if this indeed the right authority for the mint
46+
#[account(
47+
mut,
48+
address = config.custody
49+
)]
50+
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>,
4451
}
4552

4653
#[derive(AnchorDeserialize, AnchorSerialize)]
@@ -52,6 +59,9 @@ pub struct ReleaseInboundArgs {
5259

5360
#[derive(Accounts)]
5461
pub struct ReleaseInboundMint<'info> {
62+
#[account(
63+
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode,
64+
)]
5565
common: ReleaseInbound<'info>,
5666
}
5767

@@ -62,8 +72,8 @@ pub struct ReleaseInboundMint<'info> {
6272
/// Setting this flag to `false` is useful when bundling this instruction
6373
/// together with [`crate::instructions::redeem`] in a transaction, so that the minting
6474
/// is attempted optimistically.
65-
pub fn release_inbound_mint(
66-
ctx: Context<ReleaseInboundMint>,
75+
pub fn release_inbound_mint<'info>(
76+
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>,
6777
args: ReleaseInboundArgs,
6878
) -> Result<()> {
6979
let inbox_item = &mut ctx.accounts.common.inbox_item;
@@ -79,38 +89,47 @@ pub fn release_inbound_mint(
7989
}
8090

8191
assert!(inbox_item.release_status == ReleaseStatus::Released);
82-
match ctx.accounts.common.config.mode {
83-
Mode::Burning => token_interface::mint_to(
84-
CpiContext::new_with_signer(
85-
ctx.accounts.common.token_program.to_account_info(),
86-
token_interface::MintTo {
87-
mint: ctx.accounts.common.mint.to_account_info(),
88-
to: ctx.accounts.common.recipient.to_account_info(),
89-
authority: ctx.accounts.common.token_authority.clone(),
90-
},
91-
&[&[
92-
crate::TOKEN_AUTHORITY_SEED,
93-
&[ctx.bumps.common.token_authority],
94-
]],
95-
),
96-
inbox_item.amount,
92+
token_interface::mint_to(
93+
CpiContext::new_with_signer(
94+
ctx.accounts.common.token_program.to_account_info(),
95+
token_interface::MintTo {
96+
mint: ctx.accounts.common.mint.to_account_info(),
97+
to: ctx.accounts.common.custody.to_account_info(),
98+
authority: ctx.accounts.common.token_authority.clone(),
99+
},
100+
&[&[
101+
crate::TOKEN_AUTHORITY_SEED,
102+
&[ctx.bumps.common.token_authority],
103+
]],
97104
),
98-
Mode::Locking => Err(NTTError::InvalidMode.into()),
99-
}
105+
inbox_item.amount,
106+
)?;
107+
108+
onchain::invoke_transfer_checked(
109+
&ctx.accounts.common.token_program.key(),
110+
ctx.accounts.common.custody.to_account_info(),
111+
ctx.accounts.common.mint.to_account_info(),
112+
ctx.accounts.common.recipient.to_account_info(),
113+
ctx.accounts.common.token_authority.clone(),
114+
ctx.remaining_accounts,
115+
inbox_item.amount,
116+
ctx.accounts.common.mint.decimals,
117+
&[&[
118+
crate::TOKEN_AUTHORITY_SEED,
119+
&[ctx.bumps.common.token_authority],
120+
]],
121+
)?;
122+
Ok(())
100123
}
101124

102125
// Lock/unlock
103126

104127
#[derive(Accounts)]
105128
pub struct ReleaseInboundUnlock<'info> {
106-
common: ReleaseInbound<'info>,
107-
108-
/// CHECK: the token program checks if this indeed the right authority for the mint
109129
#[account(
110-
mut,
111-
address = common.config.custody
130+
constraint = common.config.mode == Mode::Locking @ NTTError::InvalidMode,
112131
)]
113-
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>,
132+
common: ReleaseInbound<'info>,
114133
}
115134

116135
/// Release an inbound transfer and unlock the tokens to the recipient.
@@ -136,25 +155,19 @@ pub fn release_inbound_unlock<'info>(
136155
}
137156
}
138157

139-
assert!(inbox_item.release_status == ReleaseStatus::Released);
140-
match ctx.accounts.common.config.mode {
141-
Mode::Burning => Err(NTTError::InvalidMode.into()),
142-
Mode::Locking => {
143-
onchain::invoke_transfer_checked(
144-
&ctx.accounts.common.token_program.key(),
145-
ctx.accounts.custody.to_account_info(),
146-
ctx.accounts.common.mint.to_account_info(),
147-
ctx.accounts.common.recipient.to_account_info(),
148-
ctx.accounts.common.token_authority.clone(),
149-
ctx.remaining_accounts,
150-
inbox_item.amount,
151-
ctx.accounts.common.mint.decimals,
152-
&[&[
153-
crate::TOKEN_AUTHORITY_SEED,
154-
&[ctx.bumps.common.token_authority],
155-
]],
156-
)?;
157-
Ok(())
158-
}
159-
}
158+
onchain::invoke_transfer_checked(
159+
&ctx.accounts.common.token_program.key(),
160+
ctx.accounts.common.custody.to_account_info(),
161+
ctx.accounts.common.mint.to_account_info(),
162+
ctx.accounts.common.recipient.to_account_info(),
163+
ctx.accounts.common.token_authority.clone(),
164+
ctx.remaining_accounts,
165+
inbox_item.amount,
166+
ctx.accounts.common.mint.decimals,
167+
&[&[
168+
crate::TOKEN_AUTHORITY_SEED,
169+
&[ctx.bumps.common.token_authority],
170+
]],
171+
)?;
172+
Ok(())
160173
}

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

+61-36
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ pub struct Transfer<'info> {
5252
#[account(mut)]
5353
pub outbox_rate_limit: Account<'info, OutboxRateLimit>,
5454

55+
#[account(
56+
mut,
57+
address = config.custody
58+
)]
59+
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>,
60+
5561
pub system_program: Program<'info, System>,
5662
}
5763

@@ -85,6 +91,9 @@ impl TransferArgs {
8591
#[derive(Accounts)]
8692
#[instruction(args: TransferArgs)]
8793
pub struct TransferBurn<'info> {
94+
#[account(
95+
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode,
96+
)]
8897
pub common: Transfer<'info>,
8998

9099
#[account(
@@ -111,15 +120,18 @@ pub struct TransferBurn<'info> {
111120
bump,
112121
)]
113122
pub session_authority: AccountInfo<'info>,
114-
}
115123

116-
pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<()> {
117-
require_eq!(
118-
ctx.accounts.common.config.mode,
119-
Mode::Burning,
120-
NTTError::InvalidMode
121-
);
124+
#[account(
125+
seeds = [crate::TOKEN_AUTHORITY_SEED],
126+
bump,
127+
)]
128+
pub token_authority: AccountInfo<'info>,
129+
}
122130

131+
pub fn transfer_burn<'info>(
132+
ctx: Context<'_, '_, '_, 'info, TransferBurn<'info>>,
133+
args: TransferArgs,
134+
) -> Result<()> {
123135
let accs = ctx.accounts;
124136
let TransferArgs {
125137
mut amount,
@@ -136,30 +148,50 @@ pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<(
136148
)
137149
.map_err(NTTError::from)?;
138150

139-
let before = accs.common.from.amount;
151+
let before = accs.common.custody.amount;
152+
153+
onchain::invoke_transfer_checked(
154+
&accs.common.token_program.key(),
155+
accs.common.from.to_account_info(),
156+
accs.common.mint.to_account_info(),
157+
accs.common.custody.to_account_info(),
158+
accs.session_authority.to_account_info(),
159+
ctx.remaining_accounts,
160+
amount,
161+
accs.common.mint.decimals,
162+
&[&[
163+
crate::SESSION_AUTHORITY_SEED,
164+
accs.common.from.owner.as_ref(),
165+
args.keccak256().as_ref(),
166+
&[ctx.bumps.session_authority],
167+
]],
168+
)?;
140169

141170
token_interface::burn(
142171
CpiContext::new_with_signer(
143172
accs.common.token_program.to_account_info(),
144173
token_interface::Burn {
145174
mint: accs.common.mint.to_account_info(),
146-
from: accs.common.from.to_account_info(),
147-
authority: accs.session_authority.to_account_info(),
175+
from: accs.common.custody.to_account_info(),
176+
authority: accs.token_authority.to_account_info(),
148177
},
149-
&[&[
150-
crate::SESSION_AUTHORITY_SEED,
151-
accs.common.from.owner.as_ref(),
152-
args.keccak256().as_ref(),
153-
&[ctx.bumps.session_authority],
154-
]],
178+
&[&[crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.token_authority]]],
155179
),
156180
amount,
157181
)?;
158182

159-
accs.common.from.reload()?;
160-
let after = accs.common.from.amount;
183+
accs.common.custody.reload()?;
184+
let after = accs.common.custody.amount;
161185

162-
if after != before - amount {
186+
// NOTE: we currently do not support tokens with fees. Support could be
187+
// added, but it would require the client to calculate the amount _before_
188+
// paying fees that results in an amount that can safely be trimmed.
189+
// Otherwise, if the amount after paying fees has dust, then that amount
190+
// would be lost.
191+
// To support fee tokens, we would first transfer the amount, _then_ assert
192+
// that the resulting amount has no dust (instead of removing dust before
193+
// the transfer like we do now).
194+
if after != before {
163195
return Err(NTTError::BadAmountAfterBurn.into());
164196
}
165197

@@ -174,14 +206,19 @@ pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<(
174206
recipient_ntt_manager,
175207
recipient_address,
176208
should_queue,
177-
)
209+
)?;
210+
211+
Ok(())
178212
}
179213

180214
// Lock/unlock
181215

182216
#[derive(Accounts)]
183217
#[instruction(args: TransferArgs)]
184218
pub struct TransferLock<'info> {
219+
#[account(
220+
constraint = common.config.mode == Mode::Locking @ NTTError::InvalidMode,
221+
)]
185222
pub common: Transfer<'info>,
186223

187224
#[account(
@@ -208,24 +245,12 @@ pub struct TransferLock<'info> {
208245
bump,
209246
)]
210247
pub session_authority: AccountInfo<'info>,
211-
212-
#[account(
213-
mut,
214-
address = common.config.custody
215-
)]
216-
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>,
217248
}
218249

219250
pub fn transfer_lock<'info>(
220251
ctx: Context<'_, '_, '_, 'info, TransferLock<'info>>,
221252
args: TransferArgs,
222253
) -> Result<()> {
223-
require_eq!(
224-
ctx.accounts.common.config.mode,
225-
Mode::Locking,
226-
NTTError::InvalidMode
227-
);
228-
229254
let accs = ctx.accounts;
230255
let TransferArgs {
231256
mut amount,
@@ -242,13 +267,13 @@ pub fn transfer_lock<'info>(
242267
)
243268
.map_err(NTTError::from)?;
244269

245-
let before = accs.custody.amount;
270+
let before = accs.common.custody.amount;
246271

247272
onchain::invoke_transfer_checked(
248273
&accs.common.token_program.key(),
249274
accs.common.from.to_account_info(),
250275
accs.common.mint.to_account_info(),
251-
accs.custody.to_account_info(),
276+
accs.common.custody.to_account_info(),
252277
accs.session_authority.to_account_info(),
253278
ctx.remaining_accounts,
254279
amount,
@@ -261,8 +286,8 @@ pub fn transfer_lock<'info>(
261286
]],
262287
)?;
263288

264-
accs.custody.reload()?;
265-
let after = accs.custody.amount;
289+
accs.common.custody.reload()?;
290+
let after = accs.common.custody.amount;
266291

267292
// NOTE: we currently do not support tokens with fees. Support could be
268293
// added, but it would require the client to calculate the amount _before_

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ pub mod example_native_token_transfers {
7777
Ok(VERSION.to_string())
7878
}
7979

80-
pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<()> {
80+
pub fn transfer_burn<'info>(
81+
ctx: Context<'_, '_, '_, 'info, TransferBurn<'info>>,
82+
args: TransferArgs,
83+
) -> Result<()> {
8184
instructions::transfer_burn(ctx, args)
8285
}
8386

@@ -92,8 +95,8 @@ pub mod example_native_token_transfers {
9295
instructions::redeem(ctx, args)
9396
}
9497

95-
pub fn release_inbound_mint(
96-
ctx: Context<ReleaseInboundMint>,
98+
pub fn release_inbound_mint<'info>(
99+
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>,
97100
args: ReleaseInboundArgs,
98101
) -> Result<()> {
99102
instructions::release_inbound_mint(ctx, args)

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub fn transfer_burn(ntt: &NTT, transfer: Transfer, args: TransferArgs) -> Instr
3333
inbox_rate_limit: ntt.inbox_rate_limit(chain_id),
3434
peer: transfer.peer,
3535
session_authority,
36+
token_authority: ntt.token_authority(),
3637
};
3738

3839
Instruction {
@@ -51,7 +52,6 @@ pub fn transfer_lock(ntt: &NTT, transfer: Transfer, args: TransferArgs) -> Instr
5152
common: common(ntt, &transfer),
5253
inbox_rate_limit: ntt.inbox_rate_limit(chain_id),
5354
peer: transfer.peer,
54-
custody: ntt.custody(&transfer.mint),
5555
session_authority,
5656
};
5757
Instruction {
@@ -90,5 +90,6 @@ fn common(ntt: &NTT, transfer: &Transfer) -> example_native_token_transfers::acc
9090
outbox_item: transfer.outbox_item,
9191
outbox_rate_limit: ntt.outbox_rate_limit(),
9292
system_program: System::id(),
93+
custody: ntt.custody(&transfer.mint),
9394
}
9495
}

0 commit comments

Comments
 (0)