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

fix(svm): close claim account manually #686

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions programs/svm-spoke/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ pub enum CustomError {
InvalidRefund,
#[msg("Zero relayer refund claim!")]
ZeroRefundClaim,
#[msg("Cannot close non-zero relayer refund claim!")]
NonZeroRefundClaim,
#[msg("Invalid claim initializer!")]
InvalidClaimInitializer,
}
37 changes: 33 additions & 4 deletions programs/svm-spoke/src/instructions/refund_claims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ pub fn claim_relayer_refund(ctx: Context<ClaimRelayerRefund>) -> Result<()> {
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]];
Expand All @@ -124,7 +121,39 @@ pub fn claim_relayer_refund(ctx: Context<ClaimRelayerRefund>) -> Result<()> {
refund_address: ctx.accounts.token_account.key(),
});

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

Ok(())
}

// TODO: add manual instruction to close claim account in case someone else executed the relayer refund leaf with ATA. Only initializer should be able to call this.
// Though claim accounts are being closed automatically when claiming the refund, there might be a scenario where
// 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)]
pub struct CloseClaimAccount<'info> {
#[account(
mut,
address = claim_account.initializer @ CustomError::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()],
bump
)]
pub claim_account: Account<'info, ClaimAccount>,
}

pub fn close_claim_account(ctx: Context<CloseClaimAccount>) -> Result<()> {
// Ensure the account does not hold any outstanding claims.
let claim_amount = ctx.accounts.claim_account.amount;
if claim_amount > 0 {
return err!(CustomError::NonZeroRefundClaim);
}

Ok(())
}
8 changes: 8 additions & 0 deletions programs/svm-spoke/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,12 @@ pub mod svm_spoke {
pub fn claim_relayer_refund(ctx: Context<ClaimRelayerRefund>) -> Result<()> {
instructions::claim_relayer_refund(ctx)
}

pub fn close_claim_account(
ctx: Context<CloseClaimAccount>,
_mint: Pubkey, // Only used in account constraints.
_token_account: Pubkey, // Only used in account constraints.
) -> Result<()> {
instructions::close_claim_account(ctx)
}
}
52 changes: 52 additions & 0 deletions test/svm/SvmSpoke.RefundClaims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,56 @@ describe("svm_spoke.refund_claims", () => {
assertSE(BigInt(iVaultBal) - BigInt(fVaultBal), relayerRefund, "Vault balance");
assertSE(BigInt(fRelayerBal) - BigInt(iRelayerBal), relayerRefund, "Relayer balance");
});

it("Close empty claim account", async () => {
// Initialize the claim account.
await initializeClaimAccount(claimInitializer);

// Should not be able to close the claim account from default wallet as the initializer was different.
try {
await program.methods.closeClaimAccount(mint, tokenAccount).accounts({ signer: payer.publicKey }).rpc();
assert.fail("Closing claim account from different initializer should fail");
} catch (error: any) {
assert.instanceOf(error, AnchorError);
assert.strictEqual(
error.error.errorCode.code,
"InvalidClaimInitializer",
"Expected error code InvalidClaimInitializer"
);
}

// Close the claim account from initializer before executing relayer refunds.
await program.methods
.closeClaimAccount(mint, tokenAccount)
.accounts({ signer: claimInitializer.publicKey })
.signers([claimInitializer])
.rpc();

// Claim account should be closed now.
try {
await program.account.claimAccount.fetch(claimAccount);
assert.fail("Claim account should be closed");
} catch (error: any) {
assert.include(error.toString(), "Account does not exist or has no data", "Expected non-existent account error");
}
});

it("Cannot close non-empty claim account", async () => {
// Execute relayer refund using claim account.
const relayerRefund = new BN(500000);
await executeRelayerRefundToClaim(relayerRefund);

// It should be not possible to close the claim account with non-zero refund liability.
try {
await program.methods
.closeClaimAccount(mint, tokenAccount)
.accounts({ signer: claimInitializer.publicKey })
.signers([claimInitializer])
.rpc();
assert.fail("Closing claim account with non-zero refund liability should fail");
} catch (error: any) {
assert.instanceOf(error, AnchorError);
assert.strictEqual(error.error.errorCode.code, "NonZeroRefundClaim", "Expected error code NonZeroRefundClaim");
}
});
});
Loading