Skip to content

Commit

Permalink
fix: separate instruction for deferred relayer refunds (#715)
Browse files Browse the repository at this point in the history
* fix: separate instruction for deferred relayer refunds

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: comment

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: update relayer refund tests

Signed-off-by: Reinis Martinsons <[email protected]>

* fix

Signed-off-by: Reinis Martinsons <[email protected]>

* test: execute relayer refunds with atomic ata creation

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: update refund script

Signed-off-by: Reinis Martinsons <[email protected]>

* fix

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: relayer can claim refunds to any account

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: check pool balance

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: do not account for token bridge balance

Signed-off-by: Reinis Martinsons <[email protected]>

---------

Signed-off-by: Reinis Martinsons <[email protected]>
  • Loading branch information
Reinis-FRP authored Nov 7, 2024
1 parent 98c761e commit d7550b8
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 309 deletions.
2 changes: 2 additions & 0 deletions programs/svm-spoke/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub enum CommonError {
DepositsArePaused,
#[msg("Fills are currently paused!")]
FillsArePaused,
#[msg("Insufficient spoke pool balance to execute leaf")]
InsufficientSpokePoolBalanceToExecuteLeaf,
}

// SVM specific errors.
Expand Down
151 changes: 94 additions & 57 deletions programs/svm-spoke/src/instructions/bundle.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use anchor_lang::{prelude::*, solana_program::keccak};
use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked};
use anchor_spl::{
associated_token,
token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked},
};

use crate::{
constants::DISCRIMINATOR_SIZE,
error::{CommonError, SvmError},
event::ExecutedRelayerRefundRoot,
state::{ExecuteRelayerRefundLeafParams, RefundAccount, RootBundle, State, TransferLiability},
state::{ClaimAccount, ExecuteRelayerRefundLeafParams, RootBundle, State, TransferLiability},
utils::{is_claimed, set_claimed, verify_merkle_proof},
};

Expand Down Expand Up @@ -71,7 +74,7 @@ pub struct RelayerRefundLeaf {
pub leaf_id: u32,
pub mint_public_key: Pubkey,
#[max_len(0)]
pub refund_accounts: Vec<Pubkey>,
pub refund_addresses: Vec<Pubkey>,
}

impl RelayerRefundLeaf {
Expand All @@ -85,8 +88,8 @@ impl RelayerRefundLeaf {
}
bytes.extend_from_slice(&self.leaf_id.to_le_bytes());
bytes.extend_from_slice(self.mint_public_key.as_ref());
for account in &self.refund_accounts {
bytes.extend_from_slice(account.as_ref());
for address in &self.refund_addresses {
bytes.extend_from_slice(address.as_ref());
}

bytes
Expand All @@ -100,6 +103,7 @@ impl RelayerRefundLeaf {

pub fn execute_relayer_refund_leaf<'c, 'info>(
ctx: Context<'_, '_, 'c, 'info, ExecuteRelayerRefundLeaf<'info>>,
deferred_refunds: bool,
) -> Result<()>
where
'c: 'info, // TODO: add explaining comments on some of more complex syntax.
Expand Down Expand Up @@ -129,62 +133,24 @@ where
relayer_refund_leaf.leaf_id,
);

// TODO: execute remaining parts of leaf structure such as amountToReturn.
// TODO: emit events.

if relayer_refund_leaf.refund_accounts.len() != relayer_refund_leaf.refund_amounts.len() {
if relayer_refund_leaf.refund_addresses.len() != relayer_refund_leaf.refund_amounts.len() {
return err!(CommonError::InvalidMerkleLeaf);
}

// Derive the signer seeds for the state. The vault owns the state PDA so we need to derive this to create the
// signer seeds to execute the CPI transfer from the vault to the refund recipient.
let state_seed_bytes = state.seed.to_le_bytes();
let seeds = &[b"state", state_seed_bytes.as_ref(), &[ctx.bumps.state]];
let signer_seeds = &[&seeds[..]];
if ctx.remaining_accounts.len() < relayer_refund_leaf.refund_addresses.len() {
return err!(ErrorCode::AccountNotEnoughKeys);
}

// Will include in the emitted event at the end if there are any claim accounts.
let mut deferred_refunds = false;
// Check if vault has sufficient balance for all the refunds.
let total_refund_amount: u64 = relayer_refund_leaf.refund_amounts.iter().sum();
if ctx.accounts.vault.amount < total_refund_amount {
return err!(CommonError::InsufficientSpokePoolBalanceToExecuteLeaf);
}

for (i, amount) in relayer_refund_leaf.refund_amounts.iter().enumerate() {
let amount = *amount as u64;

// Refund account holds either a regular token account or a claim account. This checks all required constraints.
// TODO: test ordering of the refund accounts and remaining accounts.
let refund_account = RefundAccount::try_from_remaining_account(
ctx.remaining_accounts,
i,
&relayer_refund_leaf.refund_accounts[i],
&ctx.accounts.mint.key(),
&ctx.accounts.token_program.key(),
)?;

match refund_account {
// Valid token account was passed, transfer the refund atomically.
RefundAccount::TokenAccount(token_account) => {
let transfer_accounts = TransferChecked {
from: ctx.accounts.vault.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
to: token_account.to_account_info(),
authority: ctx.accounts.state.to_account_info(),
};
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
transfer_accounts,
signer_seeds,
);
transfer_checked(cpi_context, amount, ctx.accounts.mint.decimals)?;
}
// Valid claim account was passed, increment the claim account amount.
RefundAccount::ClaimAccount(mut claim_account) => {
claim_account.amount += amount;

// Indicate in the event at the end that some refunds have been deferred.
deferred_refunds = true;

// Persist the updated claim account (Anchor handles this only for static accounts).
claim_account.exit(ctx.program_id)?;
}
}
// Depending on the called instruction flavor, we either accrue the refunds to claim accounts or transfer them.
match deferred_refunds {
true => accrue_relayer_refunds(&ctx, &relayer_refund_leaf)?,
false => distribute_relayer_refunds(&ctx, &relayer_refund_leaf)?,
}

if relayer_refund_leaf.amount_to_return > 0 {
Expand All @@ -199,10 +165,81 @@ where
root_bundle_id,
leaf_id: relayer_refund_leaf.leaf_id,
l2_token_address: ctx.accounts.mint.key(),
refund_addresses: relayer_refund_leaf.refund_accounts,
refund_addresses: relayer_refund_leaf.refund_addresses,
deferred_refunds,
caller: ctx.accounts.signer.key(),
});

Ok(())
}

fn distribute_relayer_refunds<'info>(
ctx: &Context<'_, '_, '_, 'info, ExecuteRelayerRefundLeaf<'info>>,
relayer_refund_leaf: &RelayerRefundLeaf,
) -> Result<()> {
// Derive the signer seeds for the state. The vault owns the state PDA so we need to derive this to create the
// signer seeds to execute the CPI transfer from the vault to the refund recipient's token account.
let state_seed_bytes = ctx.accounts.state.seed.to_le_bytes();
let seeds = &[b"state", state_seed_bytes.as_ref(), &[ctx.bumps.state]];
let signer_seeds = &[&seeds[..]];

for (i, amount) in relayer_refund_leaf.refund_amounts.iter().enumerate() {
// We only need to check the refund account matches the associated token address for the relayer.
// All other required checks are performed within the transfer CPI. We do not check the token account authority
// as the relayer might have transferred it to a multisig or any other wallet.
// It should be safe to access elements of refund_addresses and remaining_accounts as their lengths are checked
// before calling this internal function.
let refund_token_account = &ctx.remaining_accounts[i];
let associated_token_address = associated_token::get_associated_token_address_with_program_id(
&relayer_refund_leaf.refund_addresses[i],
&ctx.accounts.mint.key(),
&ctx.accounts.token_program.key(),
);
if refund_token_account.key() != associated_token_address {
return Err(Error::from(SvmError::InvalidRefund).with_account_name(&format!("remaining_accounts[{}]", i)));
}

let transfer_accounts = TransferChecked {
from: ctx.accounts.vault.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
to: refund_token_account.to_account_info(),
authority: ctx.accounts.state.to_account_info(),
};
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
transfer_accounts,
signer_seeds,
);
transfer_checked(cpi_context, amount.to_owned(), ctx.accounts.mint.decimals)?;
}

Ok(())
}

fn accrue_relayer_refunds<'c, 'info>(
ctx: &Context<'_, '_, 'c, 'info, ExecuteRelayerRefundLeaf<'info>>,
relayer_refund_leaf: &RelayerRefundLeaf,
) -> Result<()>
where
'c: 'info,
{
for (i, amount) in relayer_refund_leaf.refund_amounts.iter().enumerate() {
// It should be safe to access elements of refund_addresses and remaining_accounts as their lengths are checked
// before calling this internal function.
let mut claim_account = ClaimAccount::try_from(
&ctx.remaining_accounts[i],
&relayer_refund_leaf.mint_public_key,
&relayer_refund_leaf.refund_addresses[i],
)
.map_err(|e| e.with_account_name(&format!("remaining_accounts[{}]", i)))?;

claim_account.amount += amount;

// Persist the updated claim account (Anchor handles this only for static accounts).
claim_account
.exit(ctx.program_id)
.map_err(|e| e.with_account_name(&format!("remaining_accounts[{}]", i)))?;
}

Ok(())
}
98 changes: 90 additions & 8 deletions programs/svm-spoke/src/instructions/refund_claims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
};

#[derive(Accounts)]
#[instruction(mint: Pubkey, token_account: Pubkey)]
#[instruction(mint: Pubkey, refund_address: Pubkey)]
pub struct InitializeClaimAccount<'info> {
#[account(mut)]
pub signer: Signer<'info>,
Expand All @@ -18,7 +18,7 @@ pub struct InitializeClaimAccount<'info> {
init,
payer = signer,
space = DISCRIMINATOR_SIZE + ClaimAccount::INIT_SPACE,
seeds = [b"claim_account", mint.as_ref(), token_account.as_ref()],
seeds = [b"claim_account", mint.as_ref(), refund_address.as_ref()],
bump
)]
pub claim_account: Account<'info, ClaimAccount>,
Expand Down Expand Up @@ -57,14 +57,15 @@ pub struct ClaimRelayerRefund<'info> {
#[account(mint::token_program = token_program)]
pub mint: InterfaceAccount<'info, Mint>,

// Token address has been checked when executing the relayer refund leaf and it is part of claim account derivation.
// This method allows relayer to claim refunds on any custom token account.
#[account(mut, token::mint = mint, token::token_program = token_program)]
pub token_account: InterfaceAccount<'info, TokenAccount>,

// Only relayer can claim the refund with this method as the claim account is derived from the relayer's address.
#[account(
mut,
close = initializer,
seeds = [b"claim_account", mint.key().as_ref(), token_account.key().as_ref()],
seeds = [b"claim_account", mint.key().as_ref(), signer.key().as_ref()],
bump
)]
pub claim_account: Account<'info, ClaimAccount>,
Expand Down Expand Up @@ -102,7 +103,89 @@ pub fn claim_relayer_refund(ctx: Context<ClaimRelayerRefund>) -> Result<()> {
emit_cpi!(ClaimedRelayerRefund {
l2_token_address: ctx.accounts.mint.key(),
claim_amount,
refund_address: ctx.accounts.token_account.key(),
refund_address: ctx.accounts.signer.key(),
});

// There is no need to reset the claim amount as the account will be closed at the end of instruction.

Ok(())
}

#[event_cpi]
#[derive(Accounts)]
#[instruction(refund_address: Pubkey)]
pub struct ClaimRelayerRefundFor<'info> {
pub signer: Signer<'info>,

/// CHECK: We don't need any additional checks as long as this is the same account that initialized the claim account.
#[account(mut, address = claim_account.initializer @ SvmError::InvalidClaimInitializer)]
pub initializer: UncheckedAccount<'info>,

#[account(seeds = [b"state", state.seed.to_le_bytes().as_ref()], bump)]
pub state: Account<'info, State>,

#[account(
mut,
associated_token::mint = mint,
associated_token::authority = state,
associated_token::token_program = token_program
)]
pub vault: InterfaceAccount<'info, TokenAccount>,

// Mint address has been checked when executing the relayer refund leaf and it is part of claim account derivation.
#[account(mint::token_program = token_program)]
pub mint: InterfaceAccount<'info, Mint>,

#[account(
mut,
associated_token::mint = mint,
associated_token::authority = refund_address,
associated_token::token_program = token_program
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,

#[account(
mut,
close = initializer,
seeds = [b"claim_account", mint.key().as_ref(), refund_address.as_ref()],
bump
)]
pub claim_account: Account<'info, ClaimAccount>,

pub token_program: Interface<'info, TokenInterface>,
}

pub fn claim_relayer_refund_for(ctx: Context<ClaimRelayerRefundFor>, refund_address: Pubkey) -> Result<()> {
// Ensure the claim account holds a non-zero amount.
let claim_amount = ctx.accounts.claim_account.amount;
if claim_amount == 0 {
return err!(SvmError::ZeroRefundClaim);
}

// Derive the signer seeds for the state required for the transfer form vault.
let state_seed_bytes = ctx.accounts.state.seed.to_le_bytes();
let seeds = &[b"state", state_seed_bytes.as_ref(), &[ctx.bumps.state]];
let signer_seeds = &[&seeds[..]];

// Transfer the claim amount from the vault to the relayer token account.
let transfer_accounts = TransferChecked {
from: ctx.accounts.vault.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.state.to_account_info(),
};
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
transfer_accounts,
signer_seeds,
);
transfer_checked(cpi_context, claim_amount, ctx.accounts.mint.decimals)?;

// Emit the ClaimedRelayerRefund event.
emit_cpi!(ClaimedRelayerRefund {
l2_token_address: ctx.accounts.mint.key(),
claim_amount,
refund_address,
});

// There is no need to reset the claim amount as the account will be closed at the end of instruction.
Expand All @@ -114,16 +197,15 @@ pub fn claim_relayer_refund(ctx: Context<ClaimRelayerRefund>) -> Result<()> {
// relayer refunds were executed with ATA after initializing the claim account. In such cases, the initializer should be
// able to close the claim account manually.
#[derive(Accounts)]
#[instruction(mint: Pubkey, token_account: Pubkey)]
#[instruction(mint: Pubkey, refund_address: Pubkey)]
pub struct CloseClaimAccount<'info> {
#[account(mut, address = claim_account.initializer @ SvmError::InvalidClaimInitializer)]
pub signer: Signer<'info>,

#[account(
mut,
close = signer,
// TODO: We can remove mint from seed derivation as token_account itself is derived from the mint.
seeds = [b"claim_account", mint.key().as_ref(), token_account.key().as_ref()],
seeds = [b"claim_account", mint.key().as_ref(), refund_address.key().as_ref()],
bump
)]
pub claim_account: Account<'info, ClaimAccount>,
Expand Down
Loading

0 comments on commit d7550b8

Please sign in to comment.