diff --git a/examples/oft-solana/README.md b/examples/oft-solana/README.md index 03e074cb84..289fc9f1b5 100644 --- a/examples/oft-solana/README.md +++ b/examples/oft-solana/README.md @@ -100,24 +100,13 @@ anchor keys sync :warning: `--force` flag overwrites the existing keys with the ones you generate. -Run `anchor keys list` to view the generated programIds (public keys). The output should look something like this: +Optionally, run `anchor keys list` to view the generated programIds (public keys). The output should look something like this: ``` endpoint: oft: ``` -Copy the OFT's programId and go into [lib.rs](./programs/oft/src/lib.rs). Note the following snippet: - -``` -declare_id!(Pubkey::new_from_array(program_id_from_env!( - "OFT_ID", - "9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT" -))); -``` - -Replace `9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT` with the programId that you have copied. - ### Building and Deploying the Solana OFT Program Ensure you have Docker running before running the build command. diff --git a/examples/oft-solana/programs/oft/src/instructions/mod.rs b/examples/oft-solana/programs/oft/src/instructions/mod.rs index 7030177db4..0d540fe485 100644 --- a/examples/oft-solana/programs/oft/src/instructions/mod.rs +++ b/examples/oft-solana/programs/oft/src/instructions/mod.rs @@ -3,6 +3,7 @@ pub mod lz_receive; pub mod lz_receive_types; pub mod quote_oft; pub mod quote_send; +pub mod renounce_freeze; pub mod send; pub mod set_oft_config; pub mod set_pause; @@ -14,6 +15,7 @@ pub use lz_receive::*; pub use lz_receive_types::*; pub use quote_oft::*; pub use quote_send::*; +pub use renounce_freeze::*; pub use send::*; pub use set_oft_config::*; pub use set_pause::*; diff --git a/examples/oft-solana/programs/oft/src/instructions/renounce_freeze.rs b/examples/oft-solana/programs/oft/src/instructions/renounce_freeze.rs new file mode 100644 index 0000000000..ca6e9e9ece --- /dev/null +++ b/examples/oft-solana/programs/oft/src/instructions/renounce_freeze.rs @@ -0,0 +1,76 @@ +use crate::*; +use anchor_lang::solana_program; +use anchor_spl::{ + token_2022::{ + spl_token_2022::instruction::AuthorityType, + spl_token_2022::{self, solana_program::program_option::COption}, + }, + token_interface::{Mint, TokenInterface}, +}; + +#[derive(Accounts)] +#[instruction()] +pub struct RenounceFreezeAuthority<'info> { + pub admin: Signer<'info>, + #[account( + seeds = [OFT_SEED, oft_store.token_escrow.as_ref()], + bump = oft_store.bump, + has_one = admin @OFTError::Unauthorized + )] + pub oft_store: Account<'info, OFTStore>, + #[account( + mut, + constraint = token_mint.freeze_authority.is_some() @CustomError::NoFreezeAuthority, + address = oft_store.token_mint, + mint::token_program = token_program + )] + pub token_mint: InterfaceAccount<'info, Mint>, + #[account( + constraint = token_mint.freeze_authority == COption::Some(current_authority.key()) @CustomError::InvalidFreezeAuthority + )] + pub current_authority: AccountInfo<'info>, + pub token_program: Interface<'info, TokenInterface>, +} + +// +impl RenounceFreezeAuthority<'_> { + pub fn apply(ctx: Context) -> Result<()> { + let oft_store_seed = ctx.accounts.oft_store.token_escrow.key(); + let seeds: &[&[u8]] = &[ + OFT_SEED, + oft_store_seed.as_ref(), + &[ctx.accounts.oft_store.bump], + ]; + + // Create the set_authority instruction directly + let ix = spl_token_2022::instruction::set_authority( + ctx.accounts.token_program.key, + &ctx.accounts.token_mint.key(), + None, // new authority + AuthorityType::FreezeAccount, + &ctx.accounts.current_authority.key(), + &[&ctx.accounts.oft_store.key()], + )?; + + // Invoke with signing + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_mint.to_account_info(), + ctx.accounts.current_authority.to_account_info(), + ctx.accounts.oft_store.to_account_info(), + ], + &[&seeds], + )?; + + Ok(()) + } +} + +#[error_code] +pub enum CustomError { + #[msg("No freeze authority exists on this mint.")] + NoFreezeAuthority, + #[msg("The provided authority does not match the freeze authority.")] + InvalidFreezeAuthority, +} diff --git a/examples/oft-solana/programs/oft/src/lib.rs b/examples/oft-solana/programs/oft/src/lib.rs index 68b54b928f..8d4bb76ef7 100644 --- a/examples/oft-solana/programs/oft/src/lib.rs +++ b/examples/oft-solana/programs/oft/src/lib.rs @@ -14,13 +14,9 @@ use oapp::{ endpoint::{MessagingFee, MessagingReceipt}, LzReceiveParams, }; -use solana_helper::program_id_from_env; use state::*; -declare_id!(Pubkey::new_from_array(program_id_from_env!( - "OFT_ID", - "9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT" -))); +declare_id!("9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT"); pub const OFT_SEED: &[u8] = b"OFT"; pub const PEER_SEED: &[u8] = b"Peer"; @@ -62,6 +58,10 @@ pub mod oft { WithdrawFee::apply(&mut ctx, ¶ms) } + pub fn renounce_freeze(ctx: Context) -> Result<()> { + RenounceFreezeAuthority::apply(ctx) + } + // ============================== Public ============================== pub fn quote_oft(ctx: Context, params: QuoteOFTParams) -> Result { diff --git a/examples/oft-solana/tasks/index.ts b/examples/oft-solana/tasks/index.ts index 9008859b38..4314aef4ab 100644 --- a/examples/oft-solana/tasks/index.ts +++ b/examples/oft-solana/tasks/index.ts @@ -5,6 +5,7 @@ import './solana/createOFT' import './solana/createOFTAdapter' import './solana/sendOFT' import './solana/setAuthority' +import './solana/renounceFreeze' import './solana/getPrioFees' import './solana/base58' import './solana/setInboundRateLimit' diff --git a/examples/oft-solana/tasks/solana/index.ts b/examples/oft-solana/tasks/solana/index.ts index 69b2c6a898..7eb7976d5d 100644 --- a/examples/oft-solana/tasks/solana/index.ts +++ b/examples/oft-solana/tasks/solana/index.ts @@ -22,7 +22,7 @@ import { } from '@metaplex-foundation/umi' import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' import { createWeb3JsEddsa } from '@metaplex-foundation/umi-eddsa-web3js' -import { toWeb3JsInstruction, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' +import { toWeb3JsInstruction, toWeb3JsKeypair, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' import { AddressLookupTableAccount, Connection } from '@solana/web3.js' import { getSimulationComputeUnits } from '@solana-developers/helpers' import bs58 from 'bs58' @@ -63,12 +63,14 @@ export const deriveConnection = async (eid: EndpointId) => { const umi = createUmi(connection.rpcEndpoint).use(mplToolbox()) const umiWalletKeyPair = umi.eddsa.createKeypairFromSecretKey(bs58.decode(privateKey)) const umiWalletSigner = createSignerFromKeypair(umi, umiWalletKeyPair) + const web3JsKeypair = toWeb3JsKeypair(umiWalletKeyPair) umi.use(signerIdentity(umiWalletSigner)) return { connection, umi, umiWalletKeyPair, umiWalletSigner, + web3JsKeypair, } } diff --git a/examples/oft-solana/tasks/solana/renounceFreeze.ts b/examples/oft-solana/tasks/solana/renounceFreeze.ts new file mode 100644 index 0000000000..140ab3c7f3 --- /dev/null +++ b/examples/oft-solana/tasks/solana/renounceFreeze.ts @@ -0,0 +1,76 @@ +import { AnchorProvider, Program } from '@coral-xyz/anchor' +import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' +import { TOKEN_PROGRAM_ID, getMint } from '@solana/spl-token' +import { task } from 'hardhat/config' + +import { types } from '@layerzerolabs/devtools-evm-hardhat' +import { EndpointId } from '@layerzerolabs/lz-definitions' + +import { deriveConnection, getExplorerTxLink } from './index' + +interface Args { + eid: EndpointId + programId: string + oftStore: string + computeUnitPriceScaleFactor: number +} + +// TODO: no need to pass in oft store? since we can derive +task('lz:oft:solana:renounce-freeze', 'Renounce freeze authority for an OFT token') + .addParam('eid', 'The endpoint ID', undefined, types.eid) + .addParam('programId', 'The OFT program ID', undefined, types.string) + .addParam('oftStore', 'The OFT Store public key', undefined, types.string) + .addParam('computeUnitPriceScaleFactor', 'The compute unit price scale factor', 4, types.float, true) + .setAction(async ({ eid, programId: programIdStr, oftStore: oftStoreStr, computeUnitPriceScaleFactor }: Args) => { + const { connection, umiWalletSigner, web3JsKeypair } = await deriveConnection(eid) + + // TODO: clean up below block + const wallet = new NodeWallet(web3JsKeypair) + const provider = new AnchorProvider(connection, wallet, { + commitment: 'processed', + }) + + const IDL = await import('../../target/idl/oft.json').then((module) => module.default) + const types = await import('../../target/types/oft').then((module) => module) + + // @ts-expect-error we can ignore the IDL type error, which is a quick or Anchor + const program = new Program(IDL, programIdStr, provider) + + const oftStoreAccount = await program.account.oftStore.fetch(oftStoreStr) + const tokenMint = oftStoreAccount.tokenMint + + const mintAccount = await getMint(connection, tokenMint) // to support Token-2022, pass in program ID as third param here + + // we assume that the mint authority is set to the multisig + const multisig = mintAccount.mintAuthority + + if (!multisig) { + throw new Error('Mint authority is not set.') + } + + const oftStoreAccountData = await program.account.oftStore.fetch(oftStoreStr) + + const signerIsAdmin = umiWalletSigner.publicKey.toString() == oftStoreAccountData.admin.toString() + if (!signerIsAdmin) { + throw new Error('Your keypair is not the OFT Store admin.') + } + + // Call the method + try { + const tx = await program.methods + .renounceFreeze() // Method name + .accounts({ + signer: umiWalletSigner.publicKey, + oftStore: oftStoreStr, + tokenMint: tokenMint, + currentAuthority: multisig, + tokenProgram: TOKEN_PROGRAM_ID.toBase58(), // currently only supports SPL Token standard + }) + .signers([web3JsKeypair]) + .rpc() + + console.log('Transaction successful:', getExplorerTxLink(tx, eid == EndpointId.SOLANA_V2_TESTNET)) + } catch (err) { + console.error('Transaction failed:', err) + } + })