Skip to content

Commit b0ffa2c

Browse files
authored
feat: improve withdraw spl conditions when ata doesnt exist (#132)
1 parent cc8b0e2 commit b0ffa2c

File tree

3 files changed

+61
-4
lines changed

3 files changed

+61
-4
lines changed

programs/gateway/src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ pub enum Errors {
2323
EmptyReceiver,
2424
#[msg("InvalidInstructionData")]
2525
InvalidInstructionData,
26+
#[msg("InvalidAtaOwner")]
27+
InvalidAtaOwner,
2628
}

programs/gateway/src/instructions/withdraw.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{
22
contexts::{Withdraw, WithdrawSPLToken},
3+
errors::Errors,
34
state::InstructionId,
45
utils::{validate_message, verify_ata_match, DEFAULT_GAS_COST},
56
};
@@ -89,13 +90,19 @@ pub fn handle_spl(
8990
&ctx.accounts.recipient_ata.key(),
9091
)?;
9192

93+
// Check if account is either empty owned by system program or owner is token program
94+
let recipient_ata_account = ctx.accounts.recipient_ata.to_account_info();
95+
require!(
96+
*recipient_ata_account.owner == anchor_spl::token::ID
97+
|| (*recipient_ata_account.owner == anchor_lang::system_program::ID
98+
&& recipient_ata_account.lamports() == 0),
99+
Errors::InvalidAtaOwner
100+
);
101+
92102
// 3. Create recipient ATA if needed and calculate costs
93103
let mut cost_ata_create: u64 = 0;
94-
let recipient_ata_account = ctx.accounts.recipient_ata.to_account_info();
95104

96-
if recipient_ata_account.lamports() == 0
97-
|| *recipient_ata_account.owner == ctx.accounts.system_program.key()
98-
{
105+
if recipient_ata_account.lamports() == 0 {
99106
// ATA needs to be created
100107
msg!(
101108
"Creating associated token account {:?} for recipient {:?}...",

tests/gateway.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3484,6 +3484,54 @@ describe("Gateway", () => {
34843484
expect(rentPayerPdaBal0 - rentPayerPdaBal1).to.be.eq(to_ata_bal + 5000); // rentPayer pays rent
34853485
});
34863486

3487+
it("Withdrawal when a system account exists at the ATA address fails", async () => {
3488+
// new ata
3489+
const recipient = anchor.web3.Keypair.generate();
3490+
const maliciousAta = await spl.getAssociatedTokenAddress(
3491+
mint.publicKey,
3492+
recipient.publicKey,
3493+
false
3494+
);
3495+
3496+
// create system owned account using ata address
3497+
const lamports = await conn.getMinimumBalanceForRentExemption(8);
3498+
const maliciousTx = new anchor.web3.Transaction().add(
3499+
anchor.web3.SystemProgram.transfer({
3500+
fromPubkey: wallet.publicKey,
3501+
toPubkey: maliciousAta,
3502+
lamports,
3503+
})
3504+
);
3505+
await anchor.web3.sendAndConfirmTransaction(conn, maliciousTx, [wallet]);
3506+
3507+
// attempt withdrawal — should fail
3508+
const pdaAta = await spl.getAssociatedTokenAddress(
3509+
mint.publicKey,
3510+
pdaAccount,
3511+
true
3512+
);
3513+
const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount);
3514+
const amount = new anchor.BN(500_000);
3515+
const nonce = pdaAccountData.nonce;
3516+
3517+
try {
3518+
await withdrawSplToken(
3519+
mint,
3520+
usdcDecimals,
3521+
amount,
3522+
nonce,
3523+
pdaAta,
3524+
maliciousAta,
3525+
recipient.publicKey,
3526+
gatewayProgram
3527+
);
3528+
throw new Error("Expected error not thrown");
3529+
} catch (err) {
3530+
expect(err).to.be.instanceof(anchor.AnchorError);
3531+
expect(err.message).to.include("InvalidAtaOwner");
3532+
}
3533+
});
3534+
34873535
it("Withdraw SPL token with wrong nonce should fail", async () => {
34883536
let pda_ata = await spl.getAssociatedTokenAddress(
34893537
mint.publicKey,

0 commit comments

Comments
 (0)