Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

solana-ibc: add support for witness account #388

Merged
merged 11 commits into from
Oct 18, 2024
1 change: 1 addition & 0 deletions solana/restaking/programs/restaking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
witness = ["solana-ibc/witness"]
default = []

[dependencies]
Expand Down
46 changes: 31 additions & 15 deletions solana/restaking/programs/restaking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub mod restaking {
/// sent in the same order as given below
/// - Chain Data
/// - trie
/// - witness (if compiled with `witness` Cargo feature)
/// - Guest blockchain program ID
pub fn deposit<'a, 'info>(
ctx: Context<'a, 'a, 'a, 'info, Deposit<'info>>,
Expand Down Expand Up @@ -106,8 +107,12 @@ pub mod restaking {
let validator_key = match service {
Service::GuestChain { validator } => validator,
};
let remaining_accounts = validation::validate_remaining_accounts(
ctx.remaining_accounts,
&guest_chain_program_id,
)?;
let borrowed_chain_data =
ctx.remaining_accounts[0].data.try_borrow().unwrap();
remaining_accounts.chain.try_borrow_data().unwrap();
let mut chain_data: &[u8] = &borrowed_chain_data;
let chain =
solana_ibc::chain::ChainData::try_deserialize(&mut chain_data)
Expand All @@ -118,20 +123,19 @@ pub mod restaking {
let amount = validator.map_or(u128::from(amount), |val| {
u128::from(val.stake) + u128::from(amount)
});
validation::validate_remaining_accounts(
ctx.remaining_accounts,
&guest_chain_program_id,
)?;
core::mem::drop(borrowed_chain_data);

let cpi_accounts = SetStake {
sender: ctx.accounts.depositor.to_account_info(),
chain: ctx.remaining_accounts[0].clone(),
trie: ctx.remaining_accounts[1].clone(),
chain: remaining_accounts.chain.clone(),
trie: remaining_accounts.trie.clone(),
#[cfg(feature = "witness")]
witness: remaining_accounts.witness.clone(),
system_program: ctx.accounts.system_program.to_account_info(),
instruction: ctx.accounts.instruction.to_account_info(),
};
let cpi_program = ctx.remaining_accounts[2].clone();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
let cpi_ctx =
CpiContext::new(remaining_accounts.program.clone(), cpi_accounts);
solana_ibc::cpi::set_stake(cpi_ctx, validator_key, amount)
}

Expand Down Expand Up @@ -359,6 +363,8 @@ pub mod restaking {
sender: ctx.accounts.withdrawer.to_account_info(),
chain: chain.to_account_info(),
trie: ctx.accounts.trie.to_account_info(),
#[cfg(feature = "witness")]
witness: ctx.accounts.witness.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
instruction: validation::check_instructions_sysvar(
&ctx.accounts.instruction,
Expand Down Expand Up @@ -550,7 +556,6 @@ pub mod restaking {
) -> Result<()> {
let vault_params = &mut ctx.accounts.vault_params;
let staking_params = &mut ctx.accounts.staking_params;
let guest_chain = &ctx.remaining_accounts[0];

let token_account = &ctx.accounts.receipt_token_account;
if token_account.amount < 1 {
Expand All @@ -561,6 +566,10 @@ pub mod restaking {
Some(id) => id,
None => return Err(error!(ErrorCodes::OperationNotAllowed)),
};
let remaining_accounts = validation::validate_remaining_accounts(
ctx.remaining_accounts,
&guest_chain_program_id,
)?;
if vault_params.service.is_some() {
return Err(error!(ErrorCodes::ServiceAlreadySet));
}
Expand All @@ -577,7 +586,8 @@ pub mod restaking {
let validator_key = match service {
Service::GuestChain { validator } => validator,
};
let borrowed_chain_data = guest_chain.data.try_borrow().unwrap();
let borrowed_chain_data =
remaining_accounts.chain.try_borrow_data().unwrap();
let mut chain_data: &[u8] = &borrowed_chain_data;
let chain =
solana_ibc::chain::ChainData::try_deserialize(&mut chain_data)
Expand All @@ -593,15 +603,17 @@ pub mod restaking {

let cpi_accounts = SetStake {
sender: ctx.accounts.depositor.to_account_info(),
chain: guest_chain.to_account_info(),
trie: ctx.remaining_accounts[1].clone(),
chain: remaining_accounts.chain.clone(),
trie: remaining_accounts.trie.clone(),
#[cfg(feature = "witness")]
witness: remaining_accounts.witness.clone(),
system_program: ctx.accounts.system_program.to_account_info(),
instruction: validation::check_instructions_sysvar(
&ctx.accounts.instruction,
)?,
};
let cpi_program = ctx.remaining_accounts[2].clone();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
let cpi_ctx =
CpiContext::new(remaining_accounts.program.clone(), cpi_accounts);
solana_ibc::cpi::set_stake(cpi_ctx, validator_key, amount)
}

Expand Down Expand Up @@ -865,6 +877,10 @@ pub struct Withdraw<'info> {
#[account(mut, seeds = [TRIE_SEED], bump, seeds::program = guest_chain_program.key())]
/// CHECK:
pub trie: AccountInfo<'info>,
#[cfg(feature = "witness")]
#[account(mut, seeds = [solana_ibc::WITNESS_SEED, trie.key().as_ref()], bump)]
/// CHECK:
pub witness: AccountInfo<'info>,

pub token_mint: Box<Account<'info, Mint>>,
#[account(mut, token::mint = token_mint)]
Expand Down
98 changes: 74 additions & 24 deletions solana/restaking/programs/restaking/src/validation.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,101 @@
use anchor_lang::prelude::*;
use solana_ibc::{CHAIN_SEED, TRIE_SEED};

use crate::ErrorCodes;

pub(crate) struct RemainingAccounts<'a, 'info> {
pub chain: &'a AccountInfo<'info>,
pub trie: &'a AccountInfo<'info>,
#[cfg(feature = "witness")]
pub witness: &'a AccountInfo<'info>,
pub program: &'a AccountInfo<'info>,
}

/// Validates accounts needed for CPI call to the guest chain.
///
/// Right now, this method would only validate accounts for calling `set_stake`
/// method in the guest chain. Later when we expand to other services, we could
/// extend this method below to do the validation for those accounts as well.
///
/// Accounts needed for calling `set_stake`
/// - chain: PDA with seeds ["chain"]. Should be writable
/// - trie: PDA with seeds ["trie"]
/// - chain: PDA with seeds ["chain"]. Must be writable.
/// - trie: PDA with seeds ["trie"]. Must be writable.
/// - witness: Only if compiled with `witness` Cargo feature. PDA with seeds
/// `["witness", trie.key()]`. Must be writable.
/// - guest chain program ID: Should match the expected guest chain program ID
///
/// Note: The accounts should be sent in above order.
pub(crate) fn validate_remaining_accounts(
accounts: &[AccountInfo<'_>],
pub(crate) fn validate_remaining_accounts<'a, 'info>(
accounts: &'a [AccountInfo<'info>],
expected_guest_chain_program_id: &Pubkey,
) -> Result<()> {
) -> Result<RemainingAccounts<'a, 'info>> {
let accounts = &mut accounts.iter();

// Chain account
let seeds = [CHAIN_SEED];
let seeds = seeds.as_ref();
let chain = next_pda_account(
accounts,
[solana_ibc::CHAIN_SEED].as_ref(),
expected_guest_chain_program_id,
true,
"chain",
)?;

let (storage_account, _bump) =
Pubkey::find_program_address(seeds, expected_guest_chain_program_id);
if &storage_account != accounts[0].key && accounts[0].is_writable {
return Err(error!(ErrorCodes::AccountValidationFailedForCPI));
}
// Trie account
let seeds = [TRIE_SEED];
let seeds = seeds.as_ref();
let trie = next_pda_account(
accounts,
[solana_ibc::TRIE_SEED].as_ref(),
expected_guest_chain_program_id,
true,
"trie",
)?;

let (storage_account, _bump) =
Pubkey::find_program_address(seeds, expected_guest_chain_program_id);
if &storage_account != accounts[1].key && accounts[1].is_writable {
return Err(error!(ErrorCodes::AccountValidationFailedForCPI));
}
// Trie account
#[cfg(feature = "witness")]
let witness = next_pda_account(
accounts,
[solana_ibc::WITNESS_SEED, trie.key().as_ref()].as_ref(),
expected_guest_chain_program_id,
true,
"witness",
)?;

// Guest chain program ID
if expected_guest_chain_program_id != accounts[2].key {
return Err(error!(ErrorCodes::AccountValidationFailedForCPI));
}
let program = next_account_info(accounts)
.ok()
.filter(|info| expected_guest_chain_program_id == info.key)
.ok_or_else(|| error!(ErrorCodes::AccountValidationFailedForCPI))?;

Ok(())
Ok(RemainingAccounts {
chain,
trie,
program,
#[cfg(feature = "witness")]
witness,
})
}

fn next_pda_account<'a, 'info>(
accounts: &mut impl core::iter::Iterator<Item = &'a AccountInfo<'info>>,
seeds: &[&[u8]],
program_id: &Pubkey,
must_be_mut: bool,
account_name: &str,
) -> Result<&'a AccountInfo<'info>> {
(|| {
let info = next_account_info(accounts).ok()?;
let addr = Pubkey::try_find_program_address(seeds, program_id)?.0;
if &addr == info.key && (!must_be_mut || info.is_writable) {
Some(info)
} else {
None
}
})()
.ok_or_else(|| {
error!(ErrorCodes::AccountValidationFailedForCPI)
.with_account_name(account_name)
})
}


/// Verifies that given account is the Instruction sysvars and returns it if it
/// is.
pub(crate) fn check_instructions_sysvar<'info>(
Expand Down
78 changes: 49 additions & 29 deletions solana/restaking/tests/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
guestChainProgramID,
restakingProgramID,
} from "./helper";
import { Transaction, TransactionInstruction } from "@solana/web3.js";

export const depositInstruction = async (
program: anchor.Program<Restaking>,
Expand Down Expand Up @@ -159,36 +160,55 @@ export const withdrawInstruction = async (
withdrawer
);

const tx = await program.methods
.withdraw()
.preInstructions([
let instruction = new TransactionInstruction({
keys: [
{ pubkey: withdrawer, isSigner: true, isWritable: true },
{ pubkey: withdrawer, isSigner: false, isWritable: true },
{ pubkey: vaultParamsPDA, isSigner: false, isWritable: true },
{ pubkey: stakingParamsPDA, isSigner: false, isWritable: true },
{ pubkey: guestChainPDA, isSigner: false, isWritable: true },
{ pubkey: triePDA, isSigner: false, isWritable: true },
{ pubkey: stakedTokenMint, isSigner: false, isWritable: true },
{
pubkey: withdrawerStakedTokenAccount,
isSigner: false,
isWritable: true,
},
{ pubkey: vaultTokenAccountPDA, isSigner: false, isWritable: true },
{ pubkey: receiptTokenMint, isSigner: false, isWritable: true },
{ pubkey: escrowReceiptTokenPDA, isSigner: false, isWritable: true },
{ pubkey: guestChainProgramID, isSigner: false, isWritable: true },
{ pubkey: spl.TOKEN_PROGRAM_ID, isSigner: false, isWritable: true },
{
pubkey: anchor.web3.SystemProgram.programId,
isSigner: false,
isWritable: true,
},
{
pubkey: new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID),
isSigner: false,
isWritable: true,
},
{ pubkey: anchor.web3.SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: true },
{ pubkey: masterEditionPDA, isSigner: false, isWritable: true },
{ pubkey: nftMetadataPDA, isSigner: false, isWritable: true },
{
pubkey: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
isSigner: false,
isWritable: true,
},
],
programId: restakingProgramID,
data: Buffer.from([183, 18, 70, 156, 148, 109, 161, 34]),
});

let tx = new Transaction()
.add(
anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
}),
])
.accounts({
signer: withdrawer,
withdrawer,
vaultParams: vaultParamsPDA,
stakingParams: stakingParamsPDA,
guestChain: guestChainPDA,
trie: triePDA,
tokenMint: stakedTokenMint,
withdrawerTokenAccount: withdrawerStakedTokenAccount,
vaultTokenAccount: vaultTokenAccountPDA,
receiptTokenMint,
escrowReceiptTokenAccount: escrowReceiptTokenPDA,
guestChainProgram: guestChainProgramID,
tokenProgram: spl.TOKEN_PROGRAM_ID,
masterEditionAccount: masterEditionPDA,
nftMetadata: nftMetadataPDA,
systemProgram: anchor.web3.SystemProgram.programId,
metadataProgram: new anchor.web3.PublicKey(
mpl.MPL_TOKEN_METADATA_PROGRAM_ID
),
instruction: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
})
.transaction();
})
)
.add(instruction);

return tx;
};
Expand Down Expand Up @@ -315,7 +335,7 @@ export const setServiceInstruction = async (
validator: anchor.web3.PublicKey,
receiptTokenMint: anchor.web3.PublicKey,
/// Token which is staked
stakeTokenMint: anchor.web3.PublicKey,
stakeTokenMint: anchor.web3.PublicKey
) => {
const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint);
const { stakingParamsPDA } = getStakingParamsPDA();
Expand Down
Loading
Loading