-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
solana: Add multisig versions of
initialize
and release_inbound_mint
- Loading branch information
Showing
4 changed files
with
297 additions
and
0 deletions.
There are no files selected for viewing
127 changes: 127 additions & 0 deletions
127
solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
use anchor_lang::prelude::*; | ||
use anchor_spl::{associated_token::AssociatedToken, token_interface}; | ||
use ntt_messages::{chain_id::ChainId, mode::Mode}; | ||
use wormhole_solana_utils::cpi::bpf_loader_upgradeable::BpfLoaderUpgradeable; | ||
|
||
#[cfg(feature = "idl-build")] | ||
use crate::messages::Hack; | ||
|
||
use crate::{ | ||
bitmap::Bitmap, | ||
error::NTTError, | ||
queue::{outbox::OutboxRateLimit, rate_limit::RateLimitState}, | ||
}; | ||
|
||
#[derive(Accounts)] | ||
#[instruction(args: InitializeMultisigArgs)] | ||
pub struct InitializeMultisig<'info> { | ||
#[account(mut)] | ||
pub payer: Signer<'info>, | ||
|
||
#[account(address = program_data.upgrade_authority_address.unwrap_or_default())] | ||
pub deployer: Signer<'info>, | ||
|
||
#[account( | ||
seeds = [crate::ID.as_ref()], | ||
bump, | ||
seeds::program = bpf_loader_upgradeable_program, | ||
)] | ||
program_data: Account<'info, ProgramData>, | ||
|
||
#[account( | ||
init, | ||
space = 8 + crate::config::Config::INIT_SPACE, | ||
payer = payer, | ||
seeds = [crate::config::Config::SEED_PREFIX], | ||
bump | ||
)] | ||
pub config: Box<Account<'info, crate::config::Config>>, | ||
|
||
#[account( | ||
constraint = | ||
args.mode == Mode::Locking | ||
|| mint.mint_authority.unwrap() == multisig.key() | ||
@ NTTError::InvalidMintAuthority, | ||
)] | ||
pub mint: Box<InterfaceAccount<'info, token_interface::Mint>>, | ||
|
||
#[account( | ||
init, | ||
payer = payer, | ||
space = 8 + OutboxRateLimit::INIT_SPACE, | ||
seeds = [OutboxRateLimit::SEED_PREFIX], | ||
bump, | ||
)] | ||
pub rate_limit: Account<'info, OutboxRateLimit>, | ||
|
||
#[account()] | ||
/// CHECK: multisig is mint authority | ||
pub multisig: UncheckedAccount<'info>, | ||
|
||
#[account( | ||
seeds = [crate::TOKEN_AUTHORITY_SEED], | ||
bump, | ||
)] | ||
/// CHECK: [`token_authority`] is checked against the custody account and the [`mint`]'s mint_authority | ||
/// In any case, this function is used to set the Config and initialize the program so we | ||
/// assume the caller of this function will have total control over the program. | ||
/// | ||
/// TODO: Using `UncheckedAccount` here leads to "Access violation in stack frame ...". | ||
/// Could refactor code to use `Box<_>` to reduce stack size. | ||
pub token_authority: AccountInfo<'info>, | ||
|
||
#[account( | ||
init_if_needed, | ||
payer = payer, | ||
associated_token::mint = mint, | ||
associated_token::authority = token_authority, | ||
associated_token::token_program = token_program, | ||
)] | ||
/// The custody account that holds tokens in locking mode and temporarily | ||
/// holds tokens in burning mode. | ||
/// CHECK: Use init_if_needed here to prevent a denial-of-service of the [`initialize`] | ||
/// function if the token account has already been created. | ||
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, | ||
|
||
/// CHECK: checked to be the appropriate token program when initialising the | ||
/// associated token account for the given mint. | ||
pub token_program: Interface<'info, token_interface::TokenInterface>, | ||
pub associated_token_program: Program<'info, AssociatedToken>, | ||
bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, | ||
|
||
system_program: Program<'info, System>, | ||
} | ||
|
||
#[derive(AnchorSerialize, AnchorDeserialize)] | ||
pub struct InitializeMultisigArgs { | ||
pub chain_id: u16, | ||
pub limit: u64, | ||
pub mode: ntt_messages::mode::Mode, | ||
} | ||
|
||
pub fn initialize_multisig( | ||
ctx: Context<InitializeMultisig>, | ||
args: InitializeMultisigArgs, | ||
) -> Result<()> { | ||
ctx.accounts.config.set_inner(crate::config::Config { | ||
bump: ctx.bumps.config, | ||
mint: ctx.accounts.mint.key(), | ||
token_program: ctx.accounts.token_program.key(), | ||
mode: args.mode, | ||
chain_id: ChainId { id: args.chain_id }, | ||
owner: ctx.accounts.deployer.key(), | ||
pending_owner: None, | ||
paused: false, | ||
next_transceiver_id: 0, | ||
// NOTE: can't be changed for now | ||
threshold: 1, | ||
enabled_transceivers: Bitmap::new(), | ||
custody: ctx.accounts.custody.key(), | ||
}); | ||
|
||
ctx.accounts.rate_limit.set_inner(OutboxRateLimit { | ||
rate_limit: RateLimitState::new(args.limit), | ||
}); | ||
|
||
Ok(()) | ||
} |
4 changes: 4 additions & 0 deletions
4
solana/programs/example-native-token-transfers/src/instructions/mod.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,19 @@ | ||
pub mod admin; | ||
pub mod initialize; | ||
pub mod initialize_multisig; | ||
pub mod luts; | ||
pub mod mark_outbox_item_as_released; | ||
pub mod redeem; | ||
pub mod release_inbound; | ||
pub mod release_inbound_multisig; | ||
pub mod transfer; | ||
|
||
pub use admin::*; | ||
pub use initialize::*; | ||
pub use initialize_multisig::*; | ||
pub use luts::*; | ||
pub use mark_outbox_item_as_released::*; | ||
pub use redeem::*; | ||
pub use release_inbound::*; | ||
pub use release_inbound_multisig::*; | ||
pub use transfer::*; |
152 changes: 152 additions & 0 deletions
152
solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
use anchor_lang::prelude::*; | ||
use anchor_spl::token_interface; | ||
use ntt_messages::mode::Mode; | ||
use spl_token_2022::onchain; | ||
|
||
use crate::{ | ||
config::*, | ||
error::NTTError, | ||
queue::inbox::{InboxItem, ReleaseStatus}, | ||
}; | ||
|
||
#[derive(Accounts)] | ||
pub struct ReleaseInboundMultisig<'info> { | ||
#[account(mut)] | ||
pub payer: Signer<'info>, | ||
|
||
pub config: NotPausedConfig<'info>, | ||
|
||
#[account(mut)] | ||
pub inbox_item: Account<'info, InboxItem>, | ||
|
||
#[account( | ||
mut, | ||
associated_token::authority = inbox_item.recipient_address, | ||
associated_token::mint = mint, | ||
associated_token::token_program = token_program, | ||
)] | ||
pub recipient: InterfaceAccount<'info, token_interface::TokenAccount>, | ||
|
||
/// CHECK: multisig account should be mint authority | ||
#[account(constraint = mint.mint_authority.unwrap() == multisig.key())] | ||
pub multisig: UncheckedAccount<'info>, | ||
|
||
#[account( | ||
seeds = [crate::TOKEN_AUTHORITY_SEED], | ||
bump, | ||
)] | ||
/// CHECK The seeds constraint ensures that this is the correct address | ||
pub token_authority: UncheckedAccount<'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>, | ||
|
||
/// CHECK: the token program checks if this indeed the right authority for the mint | ||
#[account( | ||
mut, | ||
address = config.custody | ||
)] | ||
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, | ||
} | ||
|
||
#[derive(AnchorDeserialize, AnchorSerialize)] | ||
pub struct ReleaseInboundMultisigArgs { | ||
pub revert_on_delay: bool, | ||
} | ||
|
||
// Burn/mint | ||
|
||
#[derive(Accounts)] | ||
pub struct ReleaseInboundMultisigMint<'info> { | ||
#[account( | ||
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, | ||
)] | ||
common: ReleaseInboundMultisig<'info>, | ||
} | ||
|
||
/// Release an inbound transfer and mint the tokens to the recipient. | ||
/// When `revert_on_error` is true, the transaction will revert if the | ||
/// release timestamp has not been reached. When `revert_on_error` is false, the | ||
/// transaction succeeds, but the minting is not performed. | ||
/// Setting this flag to `false` is useful when bundling this instruction | ||
/// together with [`crate::instructions::redeem`] in a transaction, so that the minting | ||
/// is attempted optimistically. | ||
pub fn release_inbound_multisig_mint<'info>( | ||
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMultisigMint<'info>>, | ||
args: ReleaseInboundMultisigArgs, | ||
) -> Result<()> { | ||
let inbox_item = &mut ctx.accounts.common.inbox_item; | ||
|
||
let released = inbox_item.try_release()?; | ||
|
||
if !released { | ||
if args.revert_on_delay { | ||
return Err(NTTError::CantReleaseYet.into()); | ||
} else { | ||
return Ok(()); | ||
} | ||
} | ||
|
||
assert!(inbox_item.release_status == ReleaseStatus::Released); | ||
|
||
// NOTE: minting tokens is a two-step process: | ||
// 1. Mint tokens to the custody account | ||
// 2. Transfer the tokens from the custody account to the recipient | ||
// | ||
// This is done to ensure that if the token has a transfer hook defined, it | ||
// will be called after the tokens are minted. | ||
// Unfortunately the Token2022 program doesn't trigger transfer hooks when | ||
// minting tokens, so we have to do it "manually" via a transfer. | ||
// | ||
// If we didn't do this, transfer hooks could be bypassed by transferring | ||
// the tokens out through NTT first, then back in to the intended recipient. | ||
// | ||
// The [`transfer_burn`] function operates in a similar way | ||
// (transfer to custody from sender, *then* burn). | ||
|
||
// Step 1: mint tokens to the custody account | ||
let ix = spl_token_2022::instruction::mint_to( | ||
&ctx.accounts.common.token_program.key(), | ||
&ctx.accounts.common.mint.key(), | ||
&ctx.accounts.common.custody.key(), | ||
&ctx.accounts.common.multisig.key(), | ||
&[&ctx.accounts.common.token_authority.key()], | ||
inbox_item.amount, | ||
)?; | ||
solana_program::program::invoke_signed( | ||
&ix, | ||
&[ | ||
ctx.accounts.common.custody.to_account_info(), | ||
ctx.accounts.common.mint.to_account_info(), | ||
ctx.accounts.common.token_authority.to_account_info(), | ||
ctx.accounts.common.multisig.to_account_info(), | ||
], | ||
&[&[ | ||
crate::TOKEN_AUTHORITY_SEED, | ||
&[ctx.bumps.common.token_authority], | ||
]], | ||
)?; | ||
|
||
// Step 2: transfer the tokens from the custody account to the recipient | ||
onchain::invoke_transfer_checked( | ||
&ctx.accounts.common.token_program.key(), | ||
ctx.accounts.common.custody.to_account_info(), | ||
ctx.accounts.common.mint.to_account_info(), | ||
ctx.accounts.common.recipient.to_account_info(), | ||
ctx.accounts.common.token_authority.to_account_info(), | ||
ctx.remaining_accounts, | ||
inbox_item.amount, | ||
ctx.accounts.common.mint.decimals, | ||
&[&[ | ||
crate::TOKEN_AUTHORITY_SEED, | ||
&[ctx.bumps.common.token_authority], | ||
]], | ||
)?; | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters