-
Notifications
You must be signed in to change notification settings - Fork 75
fix: separate refund claim when there is no token account #633
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
Changes from 17 commits
64103df
fdf2201
3deb865
168dab2
e1be75d
120c198
1f31c01
79fa785
4c0fb22
397885a
2361d31
ec8e213
fdbd8d4
d6f5e15
e904f90
c7758ec
79476ba
cdb9ac3
d1caae6
655219d
bb1f745
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,8 @@ use anchor_lang::solana_program::keccak; | |
| use crate::{ | ||
| constants::DISCRIMINATOR_SIZE, | ||
| error::CustomError, | ||
| event::ExecutedRelayerRefundRoot, | ||
| state::{ExecuteRelayerRefundLeafParams, RootBundle, State, TransferLiability}, | ||
| event::{DeferredRelayerRefunds, ExecutedRelayerRefundRoot}, | ||
| state::{ExecuteRelayerRefundLeafParams, RefundAccount, RootBundle, State, TransferLiability}, | ||
| utils::{is_claimed, set_claimed, verify_merkle_proof}, | ||
| }; | ||
|
|
||
|
|
@@ -106,9 +106,12 @@ impl RelayerRefundLeaf { | |
| } | ||
| } | ||
|
|
||
| pub fn execute_relayer_refund_leaf<'info>( | ||
| ctx: Context<'_, '_, '_, 'info, ExecuteRelayerRefundLeaf<'info>>, | ||
| ) -> Result<()> { | ||
| pub fn execute_relayer_refund_leaf<'c, 'info>( | ||
| ctx: Context<'_, '_, 'c, 'info, ExecuteRelayerRefundLeaf<'info>>, | ||
| ) -> Result<()> | ||
| where | ||
| 'c: 'info, | ||
| { | ||
| // Get pre-loaded instruction parameters. | ||
| let instruction_params = &ctx.accounts.instruction_params; | ||
| let root_bundle_id = instruction_params.root_bundle_id; | ||
|
|
@@ -146,30 +149,48 @@ pub fn execute_relayer_refund_leaf<'info>( | |
| let seeds = &[b"state", state_seed_bytes.as_ref(), &[ctx.bumps.state]]; | ||
| let signer_seeds = &[&seeds[..]]; | ||
|
|
||
| // Will emit event at the end if there are any claim accounts. | ||
| let mut deferred_refunds = false; | ||
|
|
||
| for (i, amount) in relayer_refund_leaf.refund_amounts.iter().enumerate() { | ||
| let refund_account = relayer_refund_leaf.refund_accounts[i]; | ||
| let amount = *amount as u64; | ||
|
|
||
| // TODO: we might be able to just use the refund_account and improve this block but it's not clear yet if that's possible. | ||
| let refund_account_info = ctx | ||
| .remaining_accounts | ||
| .iter() | ||
| .find(|account| account.key == &refund_account) | ||
| .cloned() | ||
| .ok_or(CustomError::AccountNotFound)?; | ||
|
|
||
| let transfer_accounts = TransferChecked { | ||
| from: ctx.accounts.vault.to_account_info(), | ||
| mint: ctx.accounts.mint.to_account_info(), | ||
| to: refund_account_info.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)?; | ||
| // Refund account holds either a regular token account or a claim account. This checks all required constraints. | ||
| 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; | ||
|
|
||
| // Emit DeferredRelayerRefunds event at the end. | ||
| deferred_refunds = true; | ||
|
|
||
| // Persist the updated claim account (Anchor handles this only for static accounts). | ||
| claim_account.exit(ctx.program_id)?; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if relayer_refund_leaf.amount_to_return > 0 { | ||
|
|
@@ -188,5 +209,16 @@ pub fn execute_relayer_refund_leaf<'info>( | |
| caller: ctx.accounts.signer.key(), | ||
| }); | ||
|
|
||
| // Emit the DeferredRelayerRefunds event if any claim accounts were passed instead of token accounts. We cannot emit | ||
| // individual accounts and amounts as that would run out of memory for larger leaves. | ||
| if deferred_refunds { | ||
| emit_cpi!(DeferredRelayerRefunds { | ||
|
||
| chain_id: relayer_refund_leaf.chain_id, | ||
| root_bundle_id, | ||
| leaf_id: relayer_refund_leaf.leaf_id, | ||
| l2_token_address: ctx.accounts.mint.key(), | ||
| }); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| use anchor_lang::prelude::*; | ||
| use anchor_spl::token_interface::{ | ||
| transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked, | ||
| }; | ||
|
|
||
| use crate::{ | ||
| constants::DISCRIMINATOR_SIZE, | ||
| error::CustomError, | ||
| event::ClaimedRelayerRefund, | ||
| state::{ClaimAccount, State}, | ||
| }; | ||
|
|
||
| #[derive(Accounts)] | ||
| #[instruction(mint: Pubkey, token_account: Pubkey)] | ||
| pub struct InitializeClaimAccount<'info> { | ||
| #[account(mut)] | ||
| pub signer: Signer<'info>, | ||
|
|
||
| #[account( | ||
| init, | ||
| payer = signer, | ||
| space = DISCRIMINATOR_SIZE + ClaimAccount::INIT_SPACE, | ||
| seeds = [b"claim_account", mint.as_ref(), token_account.as_ref()], | ||
| bump | ||
| )] | ||
| pub claim_account: Account<'info, ClaimAccount>, | ||
|
|
||
| pub system_program: Program<'info, System>, | ||
| } | ||
|
|
||
| pub fn initialize_claim_account( | ||
| ctx: Context<InitializeClaimAccount>, | ||
| mint: Pubkey, | ||
| token_account: Pubkey, | ||
| ) -> Result<()> { | ||
| // Store the initializer so only it can receive lamports from closing the account upon claiming the refund. | ||
| ctx.accounts.claim_account.initializer = ctx.accounts.signer.key(); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| #[event_cpi] | ||
| #[derive(Accounts)] | ||
| pub struct ClaimRelayerRefund<'info> { | ||
| #[account(mut)] | ||
| 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 @ CustomError::InvalidClaimInitializer | ||
| )] | ||
| pub initializer: UncheckedAccount<'info>, | ||
|
|
||
| #[account(mut, 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>, | ||
|
|
||
| // Token address has been checked when executing the relayer refund leaf and it is part of claim account derivation. | ||
| #[account( | ||
| mut, | ||
| token::mint = mint, | ||
| token::token_program = token_program | ||
| )] | ||
| pub token_account: InterfaceAccount<'info, TokenAccount>, | ||
|
|
||
| #[account( | ||
| mut, | ||
| close = initializer, | ||
| seeds = [b"claim_account", mint.key().as_ref(), token_account.key().as_ref()], | ||
| bump | ||
| )] | ||
| pub claim_account: Account<'info, ClaimAccount>, | ||
|
|
||
| pub token_program: Interface<'info, TokenInterface>, | ||
| } | ||
|
|
||
| pub fn claim_relayer_refund(ctx: Context<ClaimRelayerRefund>) -> Result<()> { | ||
| // Ensure the claim account holds a non-zero amount. | ||
| let claim_amount = ctx.accounts.claim_account.amount; | ||
| if claim_amount == 0 { | ||
| return err!(CustomError::ZeroRefundClaim); | ||
| } | ||
|
|
||
| // Reset the claim amount. | ||
| ctx.accounts.claim_account.amount = 0; | ||
|
|
||
| // 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: ctx.accounts.token_account.key(), | ||
| }); | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you help me understand this bit of syntax a bit better? I also see it in the
lib.rs.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the constraint that lifetime for remaining accounts (
'c) is at least as long as'infofor accounts that are referenced by this account array. I think its needed because of manual remaining account processing intry_from_remaining_accountmethod. Without these lifetime constraints the compiler errors.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
o that's interesting. ye, I've had other lifetime issues while implementing other things and was able to address it through other syntax but this is more complex here.