From e9e016cdff07c21af83168a9a4c6ee2314ce019d Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 6 Nov 2024 18:38:44 -0500 Subject: [PATCH 01/15] solana: Add `set_token_authority_one_step_unchecked` ix to update mint_authority --- .../src/instructions/admin.rs | 46 ++++++++++++ .../example-native-token-transfers/src/lib.rs | 4 ++ .../json/example_native_token_transfers.json | 36 ++++++++++ .../ts/example_native_token_transfers.ts | 72 +++++++++++++++++++ 4 files changed, 158 insertions(+) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 3486e21a4..4f4c939ef 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::{token_2022::spl_token_2022::instruction::AuthorityType, token_interface}; use ntt_messages::chain_id::ChainId; use wormhole_solana_utils::cpi::bpf_loader_upgradeable::{self, BpfLoaderUpgradeable}; @@ -154,6 +155,51 @@ pub fn claim_ownership(ctx: Context) -> Result<()> { ) } +// * Set token authority +#[derive(Accounts)] +pub struct SetTokenAuthority<'info> { + #[account( + constraint = config.owner == owner.key() + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + #[account( + mut, + address = config.mint, + )] + /// CHECK: the mint address matches the config + pub mint: InterfaceAccount<'info, token_interface::Mint>, + + pub token_program: Interface<'info, token_interface::TokenInterface>, + + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + /// CHECK: The seeds constraint enforces that this is the correct account. + pub token_authority: UncheckedAccount<'info>, + + /// CHECK: This is unsafe as is so should be used with caution + pub new_authority: UncheckedAccount<'info>, +} + +pub fn set_token_authority_one_step_unchecked(ctx: Context) -> Result<()> { + token_interface::set_authority( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.mint.to_account_info(), + current_authority: ctx.accounts.token_authority.to_account_info(), + }, + &[&[crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.token_authority]]], + ), + AuthorityType::MintTokens, + Some(ctx.accounts.new_authority.key()), + ) +} + // * Set peers #[derive(Accounts)] diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 1cf9cdf4b..6a32764e0 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -126,6 +126,10 @@ pub mod example_native_token_transfers { instructions::claim_ownership(ctx) } + pub fn set_token_authority_one_step_unchecked(ctx: Context) -> Result<()> { + instructions::set_token_authority_one_step_unchecked(ctx) + } + pub fn set_paused(ctx: Context, pause: bool) -> Result<()> { instructions::set_paused(ctx, pause) } diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index cdc046fa1..f2a3977ab 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -719,6 +719,42 @@ ], "args": [] }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setPaused", "accounts": [ diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 6c8a7fce3..8edeabf8d 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -719,6 +719,42 @@ export type ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setPaused", "accounts": [ @@ -2743,6 +2779,42 @@ export const IDL: ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setPaused", "accounts": [ From 1b906e7a2178e7ecc2731ec81394f8243f05005b Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 6 Nov 2024 19:45:37 -0500 Subject: [PATCH 02/15] solana: Add constraint to ensure authority is transferred only when paused --- .../example-native-token-transfers/src/error.rs | 2 ++ .../src/instructions/admin.rs | 6 ++++-- .../idl/3_0_0/json/example_native_token_transfers.json | 5 +++++ .../ts/idl/3_0_0/ts/example_native_token_transfers.ts | 10 ++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/error.rs b/solana/programs/example-native-token-transfers/src/error.rs index 0002a12c1..28ae44376 100644 --- a/solana/programs/example-native-token-transfers/src/error.rs +++ b/solana/programs/example-native-token-transfers/src/error.rs @@ -53,6 +53,8 @@ pub enum NTTError { BitmapIndexOutOfBounds, #[msg("NoRegisteredTransceivers")] NoRegisteredTransceivers, + #[msg("NotPaused")] + NotPaused, } impl From for NTTError { diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 4f4c939ef..11c0d8ab9 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -156,10 +156,12 @@ pub fn claim_ownership(ctx: Context) -> Result<()> { } // * Set token authority + #[derive(Accounts)] pub struct SetTokenAuthority<'info> { #[account( - constraint = config.owner == owner.key() + has_one = owner, + constraint = config.paused @ NTTError::NotPaused, )] pub config: Account<'info, Config>, @@ -178,7 +180,7 @@ pub struct SetTokenAuthority<'info> { seeds = [crate::TOKEN_AUTHORITY_SEED], bump, )] - /// CHECK: The seeds constraint enforces that this is the correct account. + /// CHECK: token_program will ensure this is the valid mint_authority. pub token_authority: UncheckedAccount<'info>, /// CHECK: This is unsafe as is so should be used with caution diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index f2a3977ab..e433688a0 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -2055,6 +2055,11 @@ "code": 6023, "name": "NoRegisteredTransceivers", "msg": "NoRegisteredTransceivers" + }, + { + "code": 6024, + "name": "NotPaused", + "msg": "NotPaused" } ] } diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 8edeabf8d..b67746ee4 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -2055,6 +2055,11 @@ export type ExampleNativeTokenTransfers = { "code": 6023, "name": "NoRegisteredTransceivers", "msg": "NoRegisteredTransceivers" + }, + { + "code": 6024, + "name": "NotPaused", + "msg": "NotPaused" } ] } @@ -4115,6 +4120,11 @@ export const IDL: ExampleNativeTokenTransfers = { "code": 6023, "name": "NoRegisteredTransceivers", "msg": "NoRegisteredTransceivers" + }, + { + "code": 6024, + "name": "NotPaused", + "msg": "NotPaused" } ] } From 29a57b8d932af9eeba55523ac934237ebfbfad13 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Thu, 7 Nov 2024 14:43:51 -0500 Subject: [PATCH 03/15] solana: Add 2-step set token authority ixs --- .../src/error.rs | 4 + .../src/instructions/admin.rs | 113 ++++++- .../example-native-token-transfers/src/lib.rs | 13 +- .../src/pending_token_authority.rs | 13 + .../json/example_native_token_transfers.json | 142 ++++++++- .../ts/example_native_token_transfers.ts | 284 +++++++++++++++++- 6 files changed, 549 insertions(+), 20 deletions(-) create mode 100644 solana/programs/example-native-token-transfers/src/pending_token_authority.rs diff --git a/solana/programs/example-native-token-transfers/src/error.rs b/solana/programs/example-native-token-transfers/src/error.rs index 28ae44376..5b3efe3cf 100644 --- a/solana/programs/example-native-token-transfers/src/error.rs +++ b/solana/programs/example-native-token-transfers/src/error.rs @@ -55,6 +55,10 @@ pub enum NTTError { NoRegisteredTransceivers, #[msg("NotPaused")] NotPaused, + #[msg("InvalidPendingTokenAuthority")] + InvalidPendingTokenAuthority, + #[msg("IncorrectRentPayer")] + IncorrectRentPayer, } impl From for NTTError { diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 11c0d8ab9..8bfdb86bf 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -10,6 +10,7 @@ use crate::{ config::Config, error::NTTError, peer::NttManagerPeer, + pending_token_authority::PendingTokenAuthority, queue::{inbox::InboxRateLimit, outbox::OutboxRateLimit, rate_limit::RateLimitState}, registered_transceiver::RegisteredTransceiver, }; @@ -174,20 +175,124 @@ pub struct SetTokenAuthority<'info> { /// CHECK: the mint address matches the config pub mint: InterfaceAccount<'info, token_interface::Mint>, + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + constraint = mint.mint_authority.unwrap() == token_authority.key() @ NTTError::InvalidMintAuthority + )] + /// CHECK: The constraints enforce this is valid mint authority + pub token_authority: UncheckedAccount<'info>, + + /// CHECK: This account will be the signer in the [claim_token_authority] instruction. + pub new_authority: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct SetTokenAuthorityChecked<'info> { + pub common: SetTokenAuthority<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + #[account( + init_if_needed, + space = 8 + PendingTokenAuthority::INIT_SPACE, + payer = payer, + seeds = [PendingTokenAuthority::SEED_PREFIX], + bump + )] + pub pending_token_authority: Account<'info, PendingTokenAuthority>, + + pub system_program: Program<'info, System>, +} + +pub fn set_token_authority(ctx: Context) -> Result<()> { + ctx.accounts + .pending_token_authority + .set_inner(PendingTokenAuthority { + bump: ctx.bumps.pending_token_authority, + pending_authority: ctx.accounts.common.new_authority.key(), + rent_payer: ctx.accounts.payer.key(), + }); + Ok(()) +} + +#[derive(Accounts)] +pub struct SetTokenAuthorityUnchecked<'info> { + pub common: SetTokenAuthority<'info>, + pub token_program: Interface<'info, token_interface::TokenInterface>, +} + +pub fn set_token_authority_one_step_unchecked( + ctx: Context, +) -> Result<()> { + token_interface::set_authority( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.common.token_authority.to_account_info(), + }, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + ), + AuthorityType::MintTokens, + Some(ctx.accounts.common.new_authority.key()), + ) +} + +#[derive(Accounts)] +pub struct ClaimTokenAuthority<'info> { + #[account( + constraint = config.paused @ NTTError::NotPaused, + )] + pub config: Account<'info, Config>, + + #[account( + mut, + address = pending_token_authority.rent_payer @ NTTError::IncorrectRentPayer, + )] + pub payer: Signer<'info>, + + #[account( + mut, + address = config.mint, + )] + /// CHECK: the mint address matches the config + pub mint: InterfaceAccount<'info, token_interface::Mint>, #[account( seeds = [crate::TOKEN_AUTHORITY_SEED], bump, )] - /// CHECK: token_program will ensure this is the valid mint_authority. + /// CHECK: The seeds constraint enforces that this is the correct address pub token_authority: UncheckedAccount<'info>, - /// CHECK: This is unsafe as is so should be used with caution - pub new_authority: UncheckedAccount<'info>, + #[account( + constraint = ( + new_authority.key() == pending_token_authority.pending_authority + || new_authority.key() == token_authority.key() + ) @ NTTError::InvalidPendingTokenAuthority + )] + pub new_authority: Signer<'info>, + + #[account( + mut, + seeds = [PendingTokenAuthority::SEED_PREFIX], + bump = pending_token_authority.bump, + close = payer + )] + pub pending_token_authority: Account<'info, PendingTokenAuthority>, + + pub token_program: Interface<'info, token_interface::TokenInterface>, + + pub system_program: Program<'info, System>, } -pub fn set_token_authority_one_step_unchecked(ctx: Context) -> Result<()> { +pub fn claim_token_authority(ctx: Context) -> Result<()> { token_interface::set_authority( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 6a32764e0..69fd2af76 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -18,6 +18,7 @@ pub mod error; pub mod instructions; pub mod messages; pub mod peer; +pub mod pending_token_authority; pub mod queue; pub mod registered_transceiver; pub mod transceivers; @@ -126,10 +127,20 @@ pub mod example_native_token_transfers { instructions::claim_ownership(ctx) } - pub fn set_token_authority_one_step_unchecked(ctx: Context) -> Result<()> { + pub fn set_token_authority(ctx: Context) -> Result<()> { + instructions::set_token_authority(ctx) + } + + pub fn set_token_authority_one_step_unchecked( + ctx: Context, + ) -> Result<()> { instructions::set_token_authority_one_step_unchecked(ctx) } + pub fn claim_token_authority(ctx: Context) -> Result<()> { + instructions::claim_token_authority(ctx) + } + pub fn set_paused(ctx: Context, pause: bool) -> Result<()> { instructions::set_paused(ctx, pause) } diff --git a/solana/programs/example-native-token-transfers/src/pending_token_authority.rs b/solana/programs/example-native-token-transfers/src/pending_token_authority.rs new file mode 100644 index 000000000..0f70beddd --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/pending_token_authority.rs @@ -0,0 +1,13 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(InitSpace)] +pub struct PendingTokenAuthority { + pub bump: u8, + pub pending_authority: Pubkey, + pub rent_payer: Pubkey, +} + +impl PendingTokenAuthority { + pub const SEED_PREFIX: &'static [u8] = b"pending_token_authority"; +} diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index e433688a0..7dd412dad 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -719,8 +719,100 @@ ], "args": [] }, + { + "name": "setTokenAuthority", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "claimTokenAuthority", "accounts": [ { "name": "config", @@ -728,8 +820,8 @@ "isSigner": false }, { - "name": "owner", - "isMut": false, + "name": "payer", + "isMut": true, "isSigner": true }, { @@ -738,17 +830,27 @@ "isSigner": false }, { - "name": "tokenProgram", + "name": "tokenAuthority", "isMut": false, "isSigner": false }, { - "name": "tokenAuthority", + "name": "newAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } @@ -1412,6 +1514,26 @@ ] } }, + { + "name": "PendingTokenAuthority", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "pendingAuthority", + "type": "publicKey" + }, + { + "name": "rentPayer", + "type": "publicKey" + } + ] + } + }, { "name": "InboxItem", "type": { @@ -2060,6 +2182,16 @@ "code": 6024, "name": "NotPaused", "msg": "NotPaused" + }, + { + "code": 6025, + "name": "InvalidPendingTokenAuthority", + "msg": "InvalidPendingTokenAuthority" + }, + { + "code": 6026, + "name": "IncorrectRentPayer", + "msg": "IncorrectRentPayer" } ] } diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index b67746ee4..4690a5664 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -719,8 +719,100 @@ export type ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "setTokenAuthority", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "claimTokenAuthority", "accounts": [ { "name": "config", @@ -728,8 +820,8 @@ export type ExampleNativeTokenTransfers = { "isSigner": false }, { - "name": "owner", - "isMut": false, + "name": "payer", + "isMut": true, "isSigner": true }, { @@ -738,17 +830,27 @@ export type ExampleNativeTokenTransfers = { "isSigner": false }, { - "name": "tokenProgram", + "name": "tokenAuthority", "isMut": false, "isSigner": false }, { - "name": "tokenAuthority", + "name": "newAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } @@ -1412,6 +1514,26 @@ export type ExampleNativeTokenTransfers = { ] } }, + { + "name": "pendingTokenAuthority", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "pendingAuthority", + "type": "publicKey" + }, + { + "name": "rentPayer", + "type": "publicKey" + } + ] + } + }, { "name": "inboxItem", "type": { @@ -2060,6 +2182,16 @@ export type ExampleNativeTokenTransfers = { "code": 6024, "name": "NotPaused", "msg": "NotPaused" + }, + { + "code": 6025, + "name": "InvalidPendingTokenAuthority", + "msg": "InvalidPendingTokenAuthority" + }, + { + "code": 6026, + "name": "IncorrectRentPayer", + "msg": "IncorrectRentPayer" } ] } @@ -2784,8 +2916,100 @@ export const IDL: ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "setTokenAuthority", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "claimTokenAuthority", "accounts": [ { "name": "config", @@ -2793,8 +3017,8 @@ export const IDL: ExampleNativeTokenTransfers = { "isSigner": false }, { - "name": "owner", - "isMut": false, + "name": "payer", + "isMut": true, "isSigner": true }, { @@ -2803,17 +3027,27 @@ export const IDL: ExampleNativeTokenTransfers = { "isSigner": false }, { - "name": "tokenProgram", + "name": "tokenAuthority", "isMut": false, "isSigner": false }, { - "name": "tokenAuthority", + "name": "newAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } @@ -3477,6 +3711,26 @@ export const IDL: ExampleNativeTokenTransfers = { ] } }, + { + "name": "pendingTokenAuthority", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "pendingAuthority", + "type": "publicKey" + }, + { + "name": "rentPayer", + "type": "publicKey" + } + ] + } + }, { "name": "inboxItem", "type": { @@ -4125,6 +4379,16 @@ export const IDL: ExampleNativeTokenTransfers = { "code": 6024, "name": "NotPaused", "msg": "NotPaused" + }, + { + "code": 6025, + "name": "InvalidPendingTokenAuthority", + "msg": "InvalidPendingTokenAuthority" + }, + { + "code": 6026, + "name": "IncorrectRentPayer", + "msg": "IncorrectRentPayer" } ] } From 1dfa6f3b5123ceca710829821fc5e5c1624f4862 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Tue, 3 Dec 2024 14:47:41 -0500 Subject: [PATCH 04/15] sdk: Add `createSetTokenAuthorityInstruction` helper function --- solana/ts/lib/ntt.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index c3507c1c4..cff181b96 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -97,6 +97,8 @@ export namespace NTT { derivePda("outbox_rate_limit", programId); const tokenAuthority = (): PublicKey => derivePda("token_authority", programId); + const pendingTokenAuthority = (): PublicKey => + derivePda("pending_token_authority", programId); const peerAccount = (chain: Chain): PublicKey => derivePda(["peer", chainToBytes(chain)], programId); const registeredTransceiver = (transceiver: PublicKey): PublicKey => @@ -131,6 +133,7 @@ export namespace NTT { inboxItemAccount, sessionAuthority, tokenAuthority, + pendingTokenAuthority, peerAccount, registeredTransceiver, lutAccount, @@ -710,6 +713,34 @@ export namespace NTT { .instruction(); } + export async function createSetTokenAuthorityInstruction( + program: Program>, + config: NttBindings.Config, + args: { + payer: PublicKey; + owner: PublicKey; + newAuthority: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return await program.methods + .setTokenAuthority() + .accountsStrict({ + common: { + config: pdas.configAccount(), + tokenAuthority: pdas.tokenAuthority(), + mint: config.mint, + owner: args.owner, + newAuthority: args.newAuthority, + }, + payer: args.payer, + pendingTokenAuthority: pdas.pendingTokenAuthority(), + systemProgram: SystemProgram.programId, + }) + .instruction(); + } + export async function createSetPeerInstruction( program: Program>, args: { From 9a40f7fd68c89bfa00a8901ba0f0a2d5e7e23d96 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 11 Dec 2024 13:46:30 -0500 Subject: [PATCH 05/15] solana: Add support for cancelling mint authority transfer --- .../example-native-token-transfers/src/instructions/admin.rs | 5 +++-- solana/ts/idl/3_0_0/json/example_native_token_transfers.json | 2 +- solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 8bfdb86bf..b0ed2344a 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -183,7 +183,7 @@ pub struct SetTokenAuthority<'info> { /// CHECK: The constraints enforce this is valid mint authority pub token_authority: UncheckedAccount<'info>, - /// CHECK: This account will be the signer in the [claim_token_authority] instruction. + /// CHECK: The rent payer of the [PendingTokenAuthority] storing this account will be the signer in the [claim_token_authority] instruction. pub new_authority: UncheckedAccount<'info>, } @@ -277,7 +277,8 @@ pub struct ClaimTokenAuthority<'info> { || new_authority.key() == token_authority.key() ) @ NTTError::InvalidPendingTokenAuthority )] - pub new_authority: Signer<'info>, + /// CHECK: constraint ensures that this is the correct address + pub new_authority: UncheckedAccount<'info>, #[account( mut, diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index 7dd412dad..0a65e1d52 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -837,7 +837,7 @@ { "name": "newAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { "name": "pendingTokenAuthority", diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 4690a5664..0572264e4 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -837,7 +837,7 @@ export type ExampleNativeTokenTransfers = { { "name": "newAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { "name": "pendingTokenAuthority", @@ -3034,7 +3034,7 @@ export const IDL: ExampleNativeTokenTransfers = { { "name": "newAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { "name": "pendingTokenAuthority", From 496e3156e86f965bd2ba854559daaefc97e4cea6 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 11 Dec 2024 20:02:25 -0500 Subject: [PATCH 06/15] solana: Add `revertTokenAuthority` to cancel set token authority flow. --- .../src/instructions/admin.rs | 43 ++++--- .../example-native-token-transfers/src/lib.rs | 4 + .../json/example_native_token_transfers.json | 60 ++++++++- .../ts/example_native_token_transfers.ts | 120 ++++++++++++++++-- 4 files changed, 189 insertions(+), 38 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index b0ed2344a..e59c098dd 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -178,12 +178,12 @@ pub struct SetTokenAuthority<'info> { #[account( seeds = [crate::TOKEN_AUTHORITY_SEED], bump, - constraint = mint.mint_authority.unwrap() == token_authority.key() @ NTTError::InvalidMintAuthority + address = mint.mint_authority.unwrap() @ NTTError::InvalidMintAuthority )] /// CHECK: The constraints enforce this is valid mint authority pub token_authority: UncheckedAccount<'info>, - /// CHECK: The rent payer of the [PendingTokenAuthority] storing this account will be the signer in the [claim_token_authority] instruction. + /// CHECK: This account will be the signer in the [claim_token_authority] instruction. pub new_authority: UncheckedAccount<'info>, } @@ -245,7 +245,7 @@ pub fn set_token_authority_one_step_unchecked( } #[derive(Accounts)] -pub struct ClaimTokenAuthority<'info> { +pub struct RevertTokenAuthority<'info> { #[account( constraint = config.paused @ NTTError::NotPaused, )] @@ -255,7 +255,8 @@ pub struct ClaimTokenAuthority<'info> { mut, address = pending_token_authority.rent_payer @ NTTError::IncorrectRentPayer, )] - pub payer: Signer<'info>, + /// CHECK: the constraint enforces that this is the correct address + pub payer: UncheckedAccount<'info>, #[account( mut, @@ -271,15 +272,6 @@ pub struct ClaimTokenAuthority<'info> { /// CHECK: The seeds constraint enforces that this is the correct address pub token_authority: UncheckedAccount<'info>, - #[account( - constraint = ( - new_authority.key() == pending_token_authority.pending_authority - || new_authority.key() == token_authority.key() - ) @ NTTError::InvalidPendingTokenAuthority - )] - /// CHECK: constraint ensures that this is the correct address - pub new_authority: UncheckedAccount<'info>, - #[account( mut, seeds = [PendingTokenAuthority::SEED_PREFIX], @@ -293,15 +285,32 @@ pub struct ClaimTokenAuthority<'info> { pub system_program: Program<'info, System>, } +pub fn revert_token_authority(_ctx: Context) -> Result<()> { + Ok(()) +} + +#[derive(Accounts)] +pub struct ClaimTokenAuthority<'info> { + pub common: RevertTokenAuthority<'info>, + + #[account( + address = common.pending_token_authority.pending_authority @ NTTError::InvalidPendingTokenAuthority + )] + pub new_authority: Signer<'info>, +} + pub fn claim_token_authority(ctx: Context) -> Result<()> { token_interface::set_authority( CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), + ctx.accounts.common.token_program.to_account_info(), token_interface::SetAuthority { - account_or_mint: ctx.accounts.mint.to_account_info(), - current_authority: ctx.accounts.token_authority.to_account_info(), + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.common.token_authority.to_account_info(), }, - &[&[crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.token_authority]]], + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], ), AuthorityType::MintTokens, Some(ctx.accounts.new_authority.key()), diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 69fd2af76..9f71a747a 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -137,6 +137,10 @@ pub mod example_native_token_transfers { instructions::set_token_authority_one_step_unchecked(ctx) } + pub fn revert_token_authority(ctx: Context) -> Result<()> { + instructions::revert_token_authority(ctx) + } + pub fn claim_token_authority(ctx: Context) -> Result<()> { instructions::claim_token_authority(ctx) } diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index 0a65e1d52..07a2f80e4 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -812,7 +812,7 @@ "args": [] }, { - "name": "claimTokenAuthority", + "name": "revertTokenAuthority", "accounts": [ { "name": "config", @@ -822,7 +822,7 @@ { "name": "payer", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "mint", @@ -834,11 +834,6 @@ "isMut": false, "isSigner": false }, - { - "name": "newAuthority", - "isMut": false, - "isSigner": false - }, { "name": "pendingTokenAuthority", "isMut": true, @@ -857,6 +852,57 @@ ], "args": [] }, + { + "name": "claimTokenAuthority", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, { "name": "setPaused", "accounts": [ diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 0572264e4..2ebe1a693 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -812,7 +812,7 @@ export type ExampleNativeTokenTransfers = { "args": [] }, { - "name": "claimTokenAuthority", + "name": "revertTokenAuthority", "accounts": [ { "name": "config", @@ -822,7 +822,7 @@ export type ExampleNativeTokenTransfers = { { "name": "payer", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "mint", @@ -834,11 +834,6 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, - { - "name": "newAuthority", - "isMut": false, - "isSigner": false - }, { "name": "pendingTokenAuthority", "isMut": true, @@ -857,6 +852,57 @@ export type ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "claimTokenAuthority", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, { "name": "setPaused", "accounts": [ @@ -3009,7 +3055,7 @@ export const IDL: ExampleNativeTokenTransfers = { "args": [] }, { - "name": "claimTokenAuthority", + "name": "revertTokenAuthority", "accounts": [ { "name": "config", @@ -3019,7 +3065,7 @@ export const IDL: ExampleNativeTokenTransfers = { { "name": "payer", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "mint", @@ -3031,11 +3077,6 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, - { - "name": "newAuthority", - "isMut": false, - "isSigner": false - }, { "name": "pendingTokenAuthority", "isMut": true, @@ -3054,6 +3095,57 @@ export const IDL: ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "claimTokenAuthority", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, { "name": "setPaused", "accounts": [ From 721791cc1ef3dfe8b6a981f3bc6146f3206af769 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 13 Dec 2024 22:59:35 -0500 Subject: [PATCH 07/15] solana: Rename `payer` to `rent_payer` for more clarity --- .../src/instructions/admin.rs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index e59c098dd..049ebd354 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -192,12 +192,12 @@ pub struct SetTokenAuthorityChecked<'info> { pub common: SetTokenAuthority<'info>, #[account(mut)] - pub payer: Signer<'info>, + pub rent_payer: Signer<'info>, #[account( init_if_needed, space = 8 + PendingTokenAuthority::INIT_SPACE, - payer = payer, + payer = rent_payer, seeds = [PendingTokenAuthority::SEED_PREFIX], bump )] @@ -212,7 +212,7 @@ pub fn set_token_authority(ctx: Context) -> Result<()> .set_inner(PendingTokenAuthority { bump: ctx.bumps.pending_token_authority, pending_authority: ctx.accounts.common.new_authority.key(), - rent_payer: ctx.accounts.payer.key(), + rent_payer: ctx.accounts.rent_payer.key(), }); Ok(()) } @@ -251,13 +251,6 @@ pub struct RevertTokenAuthority<'info> { )] pub config: Account<'info, Config>, - #[account( - mut, - address = pending_token_authority.rent_payer @ NTTError::IncorrectRentPayer, - )] - /// CHECK: the constraint enforces that this is the correct address - pub payer: UncheckedAccount<'info>, - #[account( mut, address = config.mint, @@ -272,11 +265,16 @@ pub struct RevertTokenAuthority<'info> { /// CHECK: The seeds constraint enforces that this is the correct address pub token_authority: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: the constraint enforces that this is the correct address + pub rent_payer: UncheckedAccount<'info>, + #[account( mut, seeds = [PendingTokenAuthority::SEED_PREFIX], bump = pending_token_authority.bump, - close = payer + has_one = rent_payer @ NTTError::IncorrectRentPayer, + close = rent_payer )] pub pending_token_authority: Account<'info, PendingTokenAuthority>, From 3f808800135cf6b7b8589ac0fa7ecd932f0577ff Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 13 Dec 2024 23:05:13 -0500 Subject: [PATCH 08/15] solana: Refactor redundant mint authority constraint out --- .../example-native-token-transfers/src/instructions/admin.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 049ebd354..704ee6d64 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -178,7 +178,6 @@ pub struct SetTokenAuthority<'info> { #[account( seeds = [crate::TOKEN_AUTHORITY_SEED], bump, - address = mint.mint_authority.unwrap() @ NTTError::InvalidMintAuthority )] /// CHECK: The constraints enforce this is valid mint authority pub token_authority: UncheckedAccount<'info>, @@ -189,6 +188,9 @@ pub struct SetTokenAuthority<'info> { #[derive(Accounts)] pub struct SetTokenAuthorityChecked<'info> { + #[account( + constraint = common.token_authority.key() == common.mint.mint_authority.unwrap() @ NTTError::InvalidMintAuthority + )] pub common: SetTokenAuthority<'info>, #[account(mut)] From 7acf6e5bbb74da2c5f0e69aeb906d2f57ed70005 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 13 Dec 2024 23:17:01 -0500 Subject: [PATCH 09/15] solana: Replace `address` constraint with `has_one` constraint for consistency --- .../src/instructions/admin.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 704ee6d64..75b783620 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -162,16 +162,14 @@ pub fn claim_ownership(ctx: Context) -> Result<()> { pub struct SetTokenAuthority<'info> { #[account( has_one = owner, + has_one = mint, constraint = config.paused @ NTTError::NotPaused, )] pub config: Account<'info, Config>, pub owner: Signer<'info>, - #[account( - mut, - address = config.mint, - )] + #[account(mut)] /// CHECK: the mint address matches the config pub mint: InterfaceAccount<'info, token_interface::Mint>, @@ -249,14 +247,12 @@ pub fn set_token_authority_one_step_unchecked( #[derive(Accounts)] pub struct RevertTokenAuthority<'info> { #[account( + has_one = mint, constraint = config.paused @ NTTError::NotPaused, )] pub config: Account<'info, Config>, - #[account( - mut, - address = config.mint, - )] + #[account(mut)] /// CHECK: the mint address matches the config pub mint: InterfaceAccount<'info, token_interface::Mint>, From e9ec944f8e33544db5866d716443c1009edbd6cf Mon Sep 17 00:00:00 2001 From: nvsriram Date: Sat, 14 Dec 2024 00:08:13 -0500 Subject: [PATCH 10/15] solana: Use `has_one` constraint over explicit key check --- .../example-native-token-transfers/src/instructions/admin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 75b783620..44fa9a1cc 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -426,7 +426,7 @@ pub fn register_transceiver(ctx: Context) -> Result<()> { #[derive(Accounts)] pub struct SetOutboundLimit<'info> { #[account( - constraint = config.owner == owner.key() + has_one = owner, )] pub config: Account<'info, Config>, @@ -453,7 +453,7 @@ pub fn set_outbound_limit( #[instruction(args: SetInboundLimitArgs)] pub struct SetInboundLimit<'info> { #[account( - constraint = config.owner == owner.key() + has_one = owner, )] pub config: Account<'info, Config>, From b0a0ecf67abc22064008c68463299b55bace89db Mon Sep 17 00:00:00 2001 From: nvsriram Date: Sat, 14 Dec 2024 00:08:38 -0500 Subject: [PATCH 11/15] solana: Update IDL and TS helper function --- .../json/example_native_token_transfers.json | 22 +++++----- .../ts/example_native_token_transfers.ts | 44 +++++++++---------- solana/ts/lib/ntt.ts | 4 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index 07a2f80e4..888b0f683 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -753,7 +753,7 @@ ] }, { - "name": "payer", + "name": "rentPayer", "isMut": true, "isSigner": true }, @@ -819,11 +819,6 @@ "isMut": false, "isSigner": false }, - { - "name": "payer", - "isMut": true, - "isSigner": false - }, { "name": "mint", "isMut": true, @@ -834,6 +829,11 @@ "isMut": false, "isSigner": false }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, { "name": "pendingTokenAuthority", "isMut": true, @@ -863,11 +863,6 @@ "isMut": false, "isSigner": false }, - { - "name": "payer", - "isMut": true, - "isSigner": false - }, { "name": "mint", "isMut": true, @@ -878,6 +873,11 @@ "isMut": false, "isSigner": false }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, { "name": "pendingTokenAuthority", "isMut": true, diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 2ebe1a693..03af89f5d 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -753,7 +753,7 @@ export type ExampleNativeTokenTransfers = { ] }, { - "name": "payer", + "name": "rentPayer", "isMut": true, "isSigner": true }, @@ -819,11 +819,6 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, - { - "name": "payer", - "isMut": true, - "isSigner": false - }, { "name": "mint", "isMut": true, @@ -834,6 +829,11 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, { "name": "pendingTokenAuthority", "isMut": true, @@ -863,11 +863,6 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, - { - "name": "payer", - "isMut": true, - "isSigner": false - }, { "name": "mint", "isMut": true, @@ -878,6 +873,11 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, { "name": "pendingTokenAuthority", "isMut": true, @@ -2996,7 +2996,7 @@ export const IDL: ExampleNativeTokenTransfers = { ] }, { - "name": "payer", + "name": "rentPayer", "isMut": true, "isSigner": true }, @@ -3062,11 +3062,6 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, - { - "name": "payer", - "isMut": true, - "isSigner": false - }, { "name": "mint", "isMut": true, @@ -3077,6 +3072,11 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, { "name": "pendingTokenAuthority", "isMut": true, @@ -3106,11 +3106,6 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, - { - "name": "payer", - "isMut": true, - "isSigner": false - }, { "name": "mint", "isMut": true, @@ -3121,6 +3116,11 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, { "name": "pendingTokenAuthority", "isMut": true, diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index cff181b96..5cb3c67d7 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -717,7 +717,7 @@ export namespace NTT { program: Program>, config: NttBindings.Config, args: { - payer: PublicKey; + rentPayer: PublicKey; owner: PublicKey; newAuthority: PublicKey; }, @@ -734,7 +734,7 @@ export namespace NTT { owner: args.owner, newAuthority: args.newAuthority, }, - payer: args.payer, + rentPayer: args.rentPayer, pendingTokenAuthority: pdas.pendingTokenAuthority(), systemProgram: SystemProgram.programId, }) From d89293899b41d3005f4294576f66c05662e78315 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Sat, 14 Dec 2024 02:30:20 -0500 Subject: [PATCH 12/15] solana: Add accept_token_authority ix and TS helper function --- .../src/instructions/admin.rs | 43 ++++++++++++- .../example-native-token-transfers/src/lib.rs | 4 ++ .../json/example_native_token_transfers.json | 31 ++++++++++ .../ts/example_native_token_transfers.ts | 62 +++++++++++++++++++ solana/ts/lib/ntt.ts | 21 +++++++ 5 files changed, 158 insertions(+), 3 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 44fa9a1cc..fd5cf3fe3 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -158,6 +158,43 @@ pub fn claim_ownership(ctx: Context) -> Result<()> { // * Set token authority +#[derive(Accounts)] +pub struct AcceptTokenAuthority<'info> { + #[account( + has_one = mint, + constraint = config.paused @ NTTError::NotPaused, + )] + pub config: Account<'info, Config>, + + #[account(mut)] + pub mint: InterfaceAccount<'info, token_interface::Mint>, + + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + /// CHECK: The constraints enforce this is valid mint authority + pub token_authority: UncheckedAccount<'info>, + + pub current_authority: Signer<'info>, + + pub token_program: Interface<'info, token_interface::TokenInterface>, +} + +pub fn accept_token_authority(ctx: Context) -> Result<()> { + token_interface::set_authority( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.mint.to_account_info(), + current_authority: ctx.accounts.current_authority.to_account_info(), + }, + ), + AuthorityType::MintTokens, + Some(ctx.accounts.token_authority.key()), + ) +} + #[derive(Accounts)] pub struct SetTokenAuthority<'info> { #[account( @@ -170,7 +207,6 @@ pub struct SetTokenAuthority<'info> { pub owner: Signer<'info>, #[account(mut)] - /// CHECK: the mint address matches the config pub mint: InterfaceAccount<'info, token_interface::Mint>, #[account( @@ -244,6 +280,8 @@ pub fn set_token_authority_one_step_unchecked( ) } +// * Claim token authority + #[derive(Accounts)] pub struct RevertTokenAuthority<'info> { #[account( @@ -253,7 +291,6 @@ pub struct RevertTokenAuthority<'info> { pub config: Account<'info, Config>, #[account(mut)] - /// CHECK: the mint address matches the config pub mint: InterfaceAccount<'info, token_interface::Mint>, #[account( @@ -264,7 +301,7 @@ pub struct RevertTokenAuthority<'info> { pub token_authority: UncheckedAccount<'info>, #[account(mut)] - /// CHECK: the constraint enforces that this is the correct address + /// CHECK: the `pending_token_authority` constraint enforces that this is the correct address pub rent_payer: UncheckedAccount<'info>, #[account( diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 9f71a747a..5ad846e7a 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -127,6 +127,10 @@ pub mod example_native_token_transfers { instructions::claim_ownership(ctx) } + pub fn accept_token_authority(ctx: Context) -> Result<()> { + instructions::accept_token_authority(ctx) + } + pub fn set_token_authority(ctx: Context) -> Result<()> { instructions::set_token_authority(ctx) } diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index 888b0f683..21253f7a3 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -719,6 +719,37 @@ ], "args": [] }, + { + "name": "acceptTokenAuthority", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "currentAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setTokenAuthority", "accounts": [ diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 03af89f5d..697b41bcd 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -719,6 +719,37 @@ export type ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "acceptTokenAuthority", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "currentAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setTokenAuthority", "accounts": [ @@ -2962,6 +2993,37 @@ export const IDL: ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "acceptTokenAuthority", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "currentAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setTokenAuthority", "accounts": [ diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 5cb3c67d7..814cc2b79 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -713,6 +713,27 @@ export namespace NTT { .instruction(); } + export async function createAcceptTokenAuthorityInstruction( + program: Program>, + config: NttBindings.Config, + args: { + currentAuthority: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return await program.methods + .acceptTokenAuthority() + .accountsStrict({ + config: pdas.configAccount(), + mint: config.mint, + tokenProgram: config.tokenProgram, + tokenAuthority: pdas.tokenAuthority(), + currentAuthority: args.currentAuthority, + }) + .instruction(); + } + export async function createSetTokenAuthorityInstruction( program: Program>, config: NttBindings.Config, From d0094b00ee62b85c6c0134a1a6f856336620d2e6 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 23 Dec 2024 19:28:33 -0500 Subject: [PATCH 13/15] solana: Add owner as signer for `revert_token_authority` --- .../src/instructions/admin.rs | 14 +- .../json/example_native_token_transfers.json | 70 +++++---- .../ts/example_native_token_transfers.ts | 140 ++++++++++-------- 3 files changed, 132 insertions(+), 92 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index fd5cf3fe3..e5c87d7f5 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -283,7 +283,7 @@ pub fn set_token_authority_one_step_unchecked( // * Claim token authority #[derive(Accounts)] -pub struct RevertTokenAuthority<'info> { +pub struct ClaimTokenAuthorityBase<'info> { #[account( has_one = mint, constraint = config.paused @ NTTError::NotPaused, @@ -318,13 +318,23 @@ pub struct RevertTokenAuthority<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct RevertTokenAuthority<'info> { + pub common: ClaimTokenAuthorityBase<'info>, + + #[account( + address = common.config.owner + )] + pub owner: Signer<'info>, +} + pub fn revert_token_authority(_ctx: Context) -> Result<()> { Ok(()) } #[derive(Accounts)] pub struct ClaimTokenAuthority<'info> { - pub common: RevertTokenAuthority<'info>, + pub common: ClaimTokenAuthorityBase<'info>, #[account( address = common.pending_token_authority.pending_authority @ NTTError::InvalidPendingTokenAuthority diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index 21253f7a3..fd11bb705 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -846,39 +846,49 @@ "name": "revertTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "mint", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": false - }, - { - "name": "pendingTokenAuthority", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "systemProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 697b41bcd..c098e6e5f 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -846,39 +846,49 @@ export type ExampleNativeTokenTransfers = { "name": "revertTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "mint", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": false - }, - { - "name": "pendingTokenAuthority", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "systemProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] @@ -3120,39 +3130,49 @@ export const IDL: ExampleNativeTokenTransfers = { "name": "revertTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "mint", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": false - }, - { - "name": "pendingTokenAuthority", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, + { + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "systemProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] From b0cb20491181bed132f8bf27bc9a7b9a515a0f3a Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 23 Dec 2024 19:51:50 -0500 Subject: [PATCH 14/15] solana: Replace `return await ` with direct `return` --- solana/ts/lib/ntt.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 814cc2b79..b489da91b 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -254,7 +254,7 @@ export namespace NTT { pdas = pdas ?? NTT.pdas(program.programId); const limit = new BN(args.outboundLimit.toString()); - return await program.methods + return program.methods .initialize({ chainId, limit: limit, mode }) .accountsStrict({ payer: args.payer, @@ -361,7 +361,7 @@ export namespace NTT { } } - return await program.methods + return program.methods .initializeLut(new BN(slot)) .accountsStrict({ payer: args.payer, @@ -704,7 +704,7 @@ export namespace NTT { pdas?: Pdas ) { pdas = pdas ?? NTT.pdas(program.programId); - return await program.methods + return program.methods .transferOwnership() .accounts({ config: pdas.configAccount(), @@ -722,7 +722,7 @@ export namespace NTT { pdas?: Pdas ) { pdas = pdas ?? NTT.pdas(program.programId); - return await program.methods + return program.methods .acceptTokenAuthority() .accountsStrict({ config: pdas.configAccount(), @@ -745,7 +745,7 @@ export namespace NTT { pdas?: Pdas ) { pdas = pdas ?? NTT.pdas(program.programId); - return await program.methods + return program.methods .setTokenAuthority() .accountsStrict({ common: { @@ -775,7 +775,7 @@ export namespace NTT { pdas?: Pdas ) { pdas = pdas ?? NTT.pdas(program.programId); - return await program.methods + return program.methods .setPeer({ chainId: { id: toChainId(args.chain) }, address: Array.from(args.address), @@ -802,7 +802,7 @@ export namespace NTT { pdas?: Pdas ) { pdas = pdas ?? NTT.pdas(program.programId); - return await program.methods + return program.methods .setPaused(args.paused) .accountsStrict({ owner: args.owner, @@ -820,7 +820,7 @@ export namespace NTT { pdas?: Pdas ) { pdas = pdas ?? NTT.pdas(program.programId); - return await program.methods + return program.methods .setOutboundLimit({ limit: args.limit, }) @@ -842,7 +842,7 @@ export namespace NTT { pdas?: Pdas ) { pdas = pdas ?? NTT.pdas(program.programId); - return await program.methods + return program.methods .setInboundLimit({ chainId: { id: toChainId(args.chain) }, limit: args.limit, @@ -907,7 +907,7 @@ export namespace NTT { program: Program>, pdas: Pdas ): Promise> { - return await program.account.config.fetch(pdas.configAccount()); + return program.account.config.fetch(pdas.configAccount()); } export async function getInboxItem( @@ -915,7 +915,7 @@ export namespace NTT { fromChain: Chain, nttMessage: Ntt.Message ): Promise> { - return await program.account.inboxItem.fetch( + return program.account.inboxItem.fetch( NTT.pdas(program.programId).inboxItemAccount(fromChain, nttMessage) ); } From d31d8e1351a0691b4a30d5f0c66a9cf900d3d8dc Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 23 Dec 2024 19:57:35 -0500 Subject: [PATCH 15/15] solana: Add `createRevertTokenAuthorityInstruction` helper --- solana/ts/lib/ntt.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index b489da91b..147b9a0d3 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -762,6 +762,33 @@ export namespace NTT { .instruction(); } + export async function createRevertTokenAuthorityInstruction( + program: Program>, + config: NttBindings.Config, + args: { + rentPayer: PublicKey; + owner: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .revertTokenAuthority() + .accountsStrict({ + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenAuthority: pdas.tokenAuthority(), + tokenProgram: config.tokenProgram, + systemProgram: SystemProgram.programId, + rentPayer: args.rentPayer, + pendingTokenAuthority: pdas.pendingTokenAuthority(), + }, + owner: args.owner, + }) + .instruction(); + } + export async function createSetPeerInstruction( program: Program>, args: {