Skip to content

Commit

Permalink
solana: add recovery flow
Browse files Browse the repository at this point in the history
  • Loading branch information
kcsongor committed Apr 22, 2024
1 parent a3ccab3 commit 18ebdc1
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 44 deletions.
6 changes: 4 additions & 2 deletions solana/programs/example-native-token-transfers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ crate-type = ["cdylib", "lib"]
name = "example_native_token_transfers"

[features]
default = ["mainnet"]
default = ["owner-recovery", "mainnet"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
idl-build = [
"anchor-lang/idl-build",
"anchor-spl/idl-build"
"anchor-spl/idl-build",
]
# whether the owner can recover transactions
owner-recovery = []
# cargo-test-sbf will pass this along
test-sbf = []
# networks
Expand Down
2 changes: 2 additions & 0 deletions solana/programs/example-native-token-transfers/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub enum NTTError {
OverflowScaledAmount,
#[msg("BitmapIndexOutOfBounds")]
BitmapIndexOutOfBounds,
#[msg("FeatureNotEnabled")]
FeatureNotEnabled,
}

impl From<ScalingError> for NTTError {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
pub mod admin;
pub mod initialize;
pub mod luts;
pub mod recover;
pub mod redeem;
pub mod release_inbound;
pub mod transfer;

pub use admin::*;
pub use initialize::*;
pub use luts::*;
pub use recover::*;
pub use redeem::*;
pub use release_inbound::*;
pub use transfer::*;
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//! This module implements instructions to recover transfers. Only the owner can
//! execute these instructions.
//!
//! Recovery means that the tokens are redeemed, but instead of sending them to
//! the recipient, they are sent to a recovery account. The recovery account is
//! a token account of the appropriate mint.
//!
//! This is useful in case the underlying token implements a blocklisting
//! mechanism (such as OFAC sanctions), and the recipient is blocked, meaning
//! the tokens are irredeemable.
//!
//! In such cases, the owner can recover the transfer by sending them to the
//! recovery address (typically controlled by the owner, though we're not
//! prescriptive about access control of that account).
//! Ideally, it would be nice to attempt to make the transfer to the original
//! recipient, and only allow recovery if that fails. However, solana's runtime does
//! not allow recovering from a failed CPI call, so that is not possible.
//!
//! This feature is opt-in, and hidden behind a feature flag ("owner-recovery").
//! When that flag is set to false, the instructions in this module will revert.

use anchor_lang::prelude::*;
use anchor_spl::token_interface;

use crate::instructions::release_inbound::*;

#[account]
#[derive(InitSpace)]
pub struct RecoveryAccount {
/// The bump seed for the recovery account
pub bump: u8,
/// The token account that will receive the recovered tokens
pub recovery_address: Pubkey,
}

impl RecoveryAccount {
pub const SEED: &'static [u8] = b"recovery";
}

#[derive(Accounts)]
pub struct InitializeRecoveryAccount<'info> {
#[account(mut)]
pub payer: Signer<'info>,

pub config: Account<'info, crate::config::Config>,

#[account(
constraint = owner.key() == config.owner
)]
pub owner: Signer<'info>,

#[account(
init,
payer = payer,
space = 8 + RecoveryAccount::INIT_SPACE,
seeds = [RecoveryAccount::SEED],
bump,
)]
pub recovery: Account<'info, RecoveryAccount>,

#[account(
token::mint = config.mint,
)]
pub recovery_account: InterfaceAccount<'info, token_interface::TokenAccount>,

system_program: Program<'info, System>,
}

pub fn initialize_recovery_account(ctx: Context<InitializeRecoveryAccount>) -> Result<()> {
// This is the most important instruction to check the feature flag, as the
// other instructions cannot be called if the [`RecoveryAccount`] is not
// initialized anyway.
ensure_feature_enabled()?;

ctx.accounts.recovery.set_inner(RecoveryAccount {
bump: ctx.bumps.recovery,
recovery_address: ctx.accounts.recovery_account.key(),
});
Ok(())
}

#[derive(Accounts)]
pub struct UpdateRecoveryAddress<'info> {
pub config: Account<'info, crate::config::Config>,

#[account(
constraint = owner.key() == config.owner
)]
pub owner: Signer<'info>,

#[account(mut)]
pub recovery: Account<'info, RecoveryAccount>,

#[account(
token::mint = config.mint,
)]
pub new_recovery_account: InterfaceAccount<'info, token_interface::TokenAccount>,
}

pub fn update_recovery_address(ctx: Context<UpdateRecoveryAddress>) -> Result<()> {
ensure_feature_enabled()?;

ctx.accounts.recovery.recovery_address = ctx.accounts.new_recovery_account.key();
Ok(())
}

#[derive(Accounts)]
pub struct RecoverMint<'info> {
pub release_inbound_mint: ReleaseInboundMint<'info>,

#[account(
constraint = owner.key() == release_inbound_mint.common.config.owner,
)]
pub owner: Signer<'info>,

pub recovery: Account<'info, RecoveryAccount>,

#[account(
mut,
constraint = recovery_account.key() == recovery.recovery_address,
)]
pub recovery_account: InterfaceAccount<'info, token_interface::TokenAccount>,
}

pub fn recover_mint<'info>(
ctx: Context<'_, '_, '_, 'info, RecoverMint<'info>>,
args: ReleaseInboundArgs,
) -> Result<()> {
ensure_feature_enabled()?;

let accounts = &mut ctx.accounts.release_inbound_mint;
accounts.common.recipient = ctx.accounts.recovery_account.clone();
let ctx = Context {
accounts,
bumps: ctx.bumps.release_inbound_mint,
..ctx
};
release_inbound_mint(ctx, args)
}

#[derive(Accounts)]
pub struct RecoverUnlock<'info> {
pub release_inbound_unlock: ReleaseInboundUnlock<'info>,

#[account(
constraint = owner.key() == release_inbound_unlock.common.config.owner,
)]
pub owner: Signer<'info>,

pub recovery: Account<'info, RecoveryAccount>,

#[account(
mut,
constraint = recovery_account.key() == recovery.recovery_address,
)]
pub recovery_account: InterfaceAccount<'info, token_interface::TokenAccount>,
}

pub fn recover_unlock<'info>(
ctx: Context<'_, '_, '_, 'info, RecoverUnlock<'info>>,
args: ReleaseInboundArgs,
) -> Result<()> {
ensure_feature_enabled()?;

let accounts = &mut ctx.accounts.release_inbound_unlock;
accounts.common.recipient = ctx.accounts.recovery_account.clone();
let ctx = Context {
accounts,
bumps: ctx.bumps.release_inbound_unlock,
..ctx
};
release_inbound_unlock(ctx, args)
}

fn ensure_feature_enabled() -> Result<()> {
#[cfg(not(feature = "owner-recovery"))]
return Err(crate::error::NTTError::FeatureNotEnabled.into());
#[cfg(feature = "owner-recovery")]
return Ok(());
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub struct ReleaseInboundMint<'info> {
#[account(
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode,
)]
common: ReleaseInbound<'info>,
pub common: ReleaseInbound<'info>,
}

/// Release an inbound transfer and mint the tokens to the recipient.
Expand Down Expand Up @@ -126,10 +126,11 @@ pub fn release_inbound_mint<'info>(

#[derive(Accounts)]
pub struct ReleaseInboundUnlock<'info> {
/// CHECK: the token program checks if this indeed the right authority for the mint
#[account(
constraint = common.config.mode == Mode::Locking @ NTTError::InvalidMode,
)]
common: ReleaseInbound<'info>,
pub common: ReleaseInbound<'info>,
}

/// Release an inbound transfer and unlock the tokens to the recipient.
Expand Down
26 changes: 26 additions & 0 deletions solana/programs/example-native-token-transfers/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![feature(type_changing_struct_update)]

use anchor_lang::prelude::*;

// TODO: is there a more elegant way of checking that these 3 features are mutually exclusive?
Expand Down Expand Up @@ -77,6 +79,16 @@ pub mod example_native_token_transfers {
instructions::initialize_lut(ctx, recent_slot)
}

/// Initialize the recovery account.
/// The recovery flow
pub fn initialize_recovery_account(ctx: Context<InitializeRecoveryAccount>) -> Result<()> {
instructions::initialize_recovery_account(ctx)
}

pub fn update_recovery_address(ctx: Context<UpdateRecoveryAddress>) -> Result<()> {
instructions::update_recovery_address(ctx)
}

pub fn version(_ctx: Context<Version>) -> Result<String> {
Ok(VERSION.to_string())
}
Expand Down Expand Up @@ -113,6 +125,20 @@ pub mod example_native_token_transfers {
instructions::release_inbound_unlock(ctx, args)
}

pub fn recover_unlock<'info>(
ctx: Context<'_, '_, '_, 'info, RecoverUnlock<'info>>,
args: ReleaseInboundArgs,
) -> Result<()> {
instructions::recover_unlock(ctx, args)
}

pub fn recover_mint<'info>(
ctx: Context<'_, '_, '_, 'info, RecoverMint<'info>>,
args: ReleaseInboundArgs,
) -> Result<()> {
instructions::recover_mint(ctx, args)
}

pub fn transfer_ownership(ctx: Context<TransferOwnership>) -> Result<()> {
instructions::transfer_ownership(ctx)
}
Expand Down
Loading

0 comments on commit 18ebdc1

Please sign in to comment.