Skip to content

Commit

Permalink
fix: add deposit fee and audit fixes (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
skosito authored Dec 2, 2024
1 parent 2c18683 commit 7fb0a66
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 17 deletions.
49 changes: 42 additions & 7 deletions programs/protocol-contracts-solana/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub enum Errors {
DepositPaused,
#[msg("SPLAtaAndMintAddressMismatch")]
SPLAtaAndMintAddressMismatch,
#[msg("EmptyReceiver")]
EmptyReceiver,
}

declare_id!("ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis");
Expand All @@ -38,12 +40,15 @@ declare_id!("ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis");
pub mod gateway {
use super::*;

const DEPOSIT_FEE: u64 = 2_000_000;

pub fn initialize(
ctx: Context<Initialize>,
tss_address: [u8; 20],
chain_id: u64,
) -> Result<()> {
let initialized_pda = &mut ctx.accounts.pda;

initialized_pda.nonce = 0;
initialized_pda.tss_address = tss_address;
initialized_pda.authority = ctx.accounts.signer.key();
Expand Down Expand Up @@ -177,19 +182,22 @@ pub mod gateway {
) -> Result<()> {
let pda = &mut ctx.accounts.pda;
require!(!pda.deposit_paused, Errors::DepositPaused);
require!(receiver != [0u8; 20], Errors::EmptyReceiver);

let amount_with_fees = amount + DEPOSIT_FEE;
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info().clone(),
to: ctx.accounts.pda.to_account_info().clone(),
},
);
system_program::transfer(cpi_context, amount)?;
system_program::transfer(cpi_context, amount_with_fees)?;
msg!(
"{:?} deposits {:?} lamports to PDA; receiver {:?}",
"{:?} deposits {:?} lamports to PDA with fee {:?}; receiver {:?}",
ctx.accounts.signer.key(),
amount,
DEPOSIT_FEE,
receiver,
);

Expand Down Expand Up @@ -226,6 +234,17 @@ pub mod gateway {

let pda = &mut ctx.accounts.pda;
require!(!pda.deposit_paused, Errors::DepositPaused);
require!(receiver != [0u8; 20], Errors::EmptyReceiver);

// transfer deposit_fee
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info().clone(),
to: pda.to_account_info().clone(),
},
);
system_program::transfer(cpi_context, DEPOSIT_FEE)?;

let pda_ata = get_associated_token_address(&ctx.accounts.pda.key(), &from.mint);
// must deposit to the ATA from PDA in order to receive credit
Expand Down Expand Up @@ -519,10 +538,10 @@ pub struct DepositSplToken<'info> {
#[account(mut)]
pub signer: Signer<'info>,

#[account(seeds = [b"meta"], bump)]
#[account(mut, seeds = [b"meta"], bump)]
pub pda: Account<'info, Pda>,

#[account(seeds=[b"whitelist", mint_account.key().as_ref()], bump)]
#[account(seeds = [b"whitelist", mint_account.key().as_ref()], bump)]
pub whitelist_entry: Account<'info, WhitelistEntry>, // attach whitelist entry to show the mint_account is whitelisted

pub mint_account: Account<'info, Mint>,
Expand All @@ -531,8 +550,11 @@ pub struct DepositSplToken<'info> {

#[account(mut)]
pub from: Account<'info, TokenAccount>, // this must be owned by signer; normally the ATA of signer

#[account(mut)]
pub to: Account<'info, TokenAccount>, // this must be ATA of PDA

pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
Expand All @@ -542,6 +564,7 @@ pub struct Withdraw<'info> {

#[account(mut, seeds = [b"meta"], bump)]
pub pda: Account<'info, Pda>,

/// CHECK: to account is not read so no need to check its owners; the program neither knows nor cares who the owner is.
#[account(mut)]
pub to: UncheckedAccount<'info>,
Expand All @@ -568,15 +591,19 @@ pub struct WithdrawSPLToken<'info> {

#[account(mut, seeds = [b"rent-payer"], bump)]
pub rent_payer_pda: Account<'info, RentPayerPda>,

pub token_program: Program<'info, Token>,

pub associated_token_program: Program<'info, AssociatedToken>,

pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateTss<'info> {
#[account(mut, seeds = [b"meta"], bump)]
pub pda: Account<'info, Pda>,

#[account(mut)]
pub signer: Signer<'info>,
}
Expand All @@ -585,6 +612,7 @@ pub struct UpdateTss<'info> {
pub struct UpdateAuthority<'info> {
#[account(mut, seeds = [b"meta"], bump)]
pub pda: Account<'info, Pda>,

#[account(mut)]
pub signer: Signer<'info>,
}
Expand All @@ -593,6 +621,7 @@ pub struct UpdateAuthority<'info> {
pub struct UpdatePaused<'info> {
#[account(mut, seeds = [b"meta"], bump)]
pub pda: Account<'info, Pda>,

#[account(mut)]
pub signer: Signer<'info>,
}
Expand All @@ -601,19 +630,21 @@ pub struct UpdatePaused<'info> {
pub struct Whitelist<'info> {
#[account(
init,
space=8,
space = 8,
payer=authority,
seeds=[
seeds = [
b"whitelist",
whitelist_candidate.key().as_ref()
],
bump
)]
pub whitelist_entry: Account<'info, WhitelistEntry>,

pub whitelist_candidate: Account<'info, Mint>,

#[account(mut, seeds = [b"meta"], bump)]
pub pda: Account<'info, Pda>,

#[account(mut)]
pub authority: Signer<'info>,

Expand All @@ -624,18 +655,20 @@ pub struct Whitelist<'info> {
pub struct Unwhitelist<'info> {
#[account(
mut,
seeds=[
seeds = [
b"whitelist",
whitelist_candidate.key().as_ref()
],
bump,
close = authority,
)]
pub whitelist_entry: Account<'info, WhitelistEntry>,

pub whitelist_candidate: Account<'info, Mint>,

#[account(mut, seeds = [b"meta"], bump)]
pub pda: Account<'info, Pda>,

#[account(mut)]
pub authority: Signer<'info>,

Expand All @@ -646,8 +679,10 @@ pub struct Unwhitelist<'info> {
pub struct InitializeRentPayer<'info> {
#[account(init, payer = authority, space = 8, seeds = [b"rent-payer"], bump)]
pub rent_payer_pda: Account<'info, RentPayerPda>,

#[account(mut)]
pub authority: Signer<'info>,

pub system_program: Program<'info, System>,
}

Expand Down
54 changes: 44 additions & 10 deletions tests/protocol-contracts-solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ async function depositSplTokens(gatewayProgram: Program<Gateway>, conn: anchor.w
}).rpc({commitment: 'processed'});
return;
}

async function withdrawSplToken( mint, decimals, amount, nonce,from, to, to_owner, tssKey, gatewayProgram: Program<Gateway>) {
const buffer = Buffer.concat([
Buffer.from("withdraw_spl_token","utf-8"),
Expand Down Expand Up @@ -92,7 +93,7 @@ async function withdrawSplToken( mint, decimals, amount, nonce,from, to, to_owne
}


describe("some tests", () => {
describe("Gateway", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const conn = anchor.getProvider().connection;
Expand Down Expand Up @@ -275,6 +276,7 @@ describe("some tests", () => {
to: fake_pda_ata.address,
mintAccount: mint_fake.publicKey,
}).rpc({commitment: 'processed'});
throw new Error("Expected error not thrown");
} catch (err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("AccountNotInitialized");
Expand Down Expand Up @@ -369,10 +371,21 @@ describe("some tests", () => {
expect(rentPayerPdaBal0-rentPayerPdaBal1).to.be.eq(to_ata_bal); // rentPayer pays rent
});

it("fails to deposit if receiver is empty address", async() => {
try {
await gatewayProgram.methods.deposit(new anchor.BN(1_000_000_000), Array(20).fill(0)).accounts({}).rpc();
throw new Error("Expected error not thrown");
} catch(err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("EmptyReceiver");
}
});

it("deposit and withdraw 0.5 SOL from Gateway with ECDSA signature", async () => {
await gatewayProgram.methods.deposit(new anchor.BN(1_000_000_000), Array.from(address)).accounts({}).rpc();
let bal1 = await conn.getBalance(pdaAccount);
expect(bal1).to.be.gte(1_000_000_000);
// amount + deposit fee
expect(bal1).to.be.gte(1_000_000_000 + 2_000_000);
const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount);
const nonce = pdaAccountData.nonce;
const amount = new anchor.BN(500000000);
Expand Down Expand Up @@ -402,6 +415,16 @@ describe("some tests", () => {
let bal3 = await conn.getBalance(to);
expect(bal3).to.be.gte(500_000_000);
})

it("fails to deposit and call if receiver is empty address", async() => {
try {
await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000_000), Array(20).fill(0), Buffer.from("hello", "utf-8")).accounts({}).rpc();
throw new Error("Expected error not thrown");
} catch(err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("EmptyReceiver");
}
});

it("deposit and call", async () => {
let bal1 = await conn.getBalance(pdaAccount);
Expand All @@ -411,13 +434,24 @@ describe("some tests", () => {
expect(bal2-bal1).to.be.gte(1_000_000_000);
})

it("fails to deposit spl if receiver is empty address", async () => {
try {
await depositSplTokens(gatewayProgram, conn, wallet, mint, Buffer.alloc(20))
throw new Error("Expected error not thrown");
} catch (err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("EmptyReceiver");
}
});

it("unwhitelist SPL token and deposit should fail", async () => {
await gatewayProgram.methods.unwhitelistSplMint([], 0, [], new anchor.BN(0)).accounts({
whitelistCandidate: mint.publicKey,
}).rpc();

try {
await depositSplTokens(gatewayProgram, conn, wallet, mint, address)
await depositSplTokens(gatewayProgram, conn, wallet, mint, address);
throw new Error("Expected error not thrown");
} catch (err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("AccountNotInitialized");
Expand Down Expand Up @@ -459,7 +493,8 @@ describe("some tests", () => {
}).rpc();

try {
await depositSplTokens(gatewayProgram, conn, wallet, mint, address)
await depositSplTokens(gatewayProgram, conn, wallet, mint, address);
throw new Error("Expected error not thrown");
} catch (err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("AccountNotInitialized");
Expand Down Expand Up @@ -509,6 +544,7 @@ describe("some tests", () => {
await gatewayProgram.methods.updateTss(Array.from(newTss)).accounts({
signer: mint.publicKey,
}).signers([mint]).rpc();
throw new Error("Expected error not thrown");
} catch (err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("SignerIsNotAuthority");
Expand All @@ -525,6 +561,7 @@ describe("some tests", () => {
// now try deposit, should fail
try {
await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000), Array.from(address), Buffer.from('hi', 'utf-8')).accounts({}).rpc();
throw new Error("Expected error not thrown");
} catch (err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("DepositPaused");
Expand All @@ -533,14 +570,11 @@ describe("some tests", () => {

const newAuthority = anchor.web3.Keypair.generate();
it("update authority", async () => {
await gatewayProgram.methods.updateAuthority(newAuthority.publicKey).accounts({

}).rpc();
await gatewayProgram.methods.updateAuthority(newAuthority.publicKey).accounts({}).rpc();
// now the old authority cannot update TSS address and will fail
try {
await gatewayProgram.methods.updateTss(Array.from(new Uint8Array(20))).accounts({

}).rpc();
await gatewayProgram.methods.updateTss(Array.from(new Uint8Array(20))).accounts({}).rpc();
throw new Error("Expected error not thrown");
} catch (err) {
expect(err).to.be.instanceof(anchor.AnchorError);
expect(err.message).to.include("SignerIsNotAuthority");
Expand Down

0 comments on commit 7fb0a66

Please sign in to comment.