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

Add security.txt, further restrict flash loans #154

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
We pay a bug bounty at our discretion after verifying the bug, up to 10% of value at risk, limited by a maximum of USD $250,000. This bounty is only paid out if details about the security issues have not been provided to third parties before a fix has been introduced and verified. Furthermore, the reporter is in no way allowed to exploit the issue without our explicit consent.

Additionally, the following are out of scope for the bug bounty:

- Any submission violating [Immunefi's rules](https://immunefi.com/rules/)
1 change: 1 addition & 0 deletions programs/unstake/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ anchor-lang = { version = "0.28.0", features = ["init-if-needed"] }
anchor-spl = { version = "0.28.0", features = ["metadata", "stake", "token"] }
mpl-token-metadata = { version = "^1.13", features = ["no-entrypoint"] }
serde = { version = "1.0.171", features = ["derive"] }
solana-security-txt = "1.1.1"
spl-associated-token-account = "^1.1" # required for anchor-spl token
3 changes: 3 additions & 0 deletions programs/unstake/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,7 @@ pub enum UnstakeError {

#[msg("No succeeding repay flash loan instruction found")]
NoSucceedingRepayFlashLoan, // 0x177e

#[msg("Flash loan active, no further flash loans and liquidity addition allowed")]
FlashLoanActive, // 0x177f
}
4 changes: 4 additions & 0 deletions programs/unstake/src/instructions/add_liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ impl<'info> AddLiquidity<'info> {
let token_program = &ctx.accounts.token_program;
let system_program = &ctx.accounts.system_program;

if !flash_account.data_is_empty() {
return Err(UnstakeError::FlashLoanActive.into());
}

// order matters, must calculate first before mutation
let pool_owned_lamports =
calc_pool_owned_lamports(pool_sol_reserves, pool_account, flash_account)?;
Expand Down
68 changes: 35 additions & 33 deletions programs/unstake/src/instructions/flash_loan/take_flash_loan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ impl<'info> TakeFlashLoan<'info> {
let instructions = &ctx.accounts.instructions;
let system_program = &ctx.accounts.system_program;

if !flash_account.data_is_empty() {
return Err(UnstakeError::FlashLoanActive.into());
}

// Check corresponding repay instruction exists
let current_idx: usize = load_current_index_checked(instructions.as_ref())?.into();
let mut next_ix_idx = current_idx + 1;
Expand All @@ -86,39 +90,37 @@ impl<'info> TakeFlashLoan<'info> {
.ok_or(UnstakeError::PdaBumpNotCached)?],
];

// init flash_account if required
if flash_account.data_is_empty() {
// you can only invoke_signed with one seed, so
// we need to split create_account up into
// allocate, assign, transfer
let flash_account_seeds: &[&[u8]] = &[
&pool_account.key().to_bytes(),
FLASH_ACCOUNT_SEED_SUFFIX,
&[*ctx
.bumps
.get("flash_account")
.ok_or(UnstakeError::PdaBumpNotCached)?],
];
allocate_assign_pda(AllocateAssignPdaArgs {
system_program,
pda_account: flash_account,
pda_account_owner_program: &crate::ID,
pda_account_len: FlashAccount::account_len(),
pda_account_signer_seeds: &[flash_account_seeds],
})?;
if flash_account.lamports() == 0 {
transfer(
CpiContext::new_with_signer(
system_program.to_account_info(),
Transfer {
from: pool_sol_reserves.to_account_info(),
to: flash_account.to_account_info(),
},
&[seeds],
),
1, // 1 lamport hot potato
)?;
}
// init flash_account
// you can only invoke_signed with one seed, so
// we need to split create_account up into
// allocate, assign, transfer
let flash_account_seeds: &[&[u8]] = &[
&pool_account.key().to_bytes(),
FLASH_ACCOUNT_SEED_SUFFIX,
&[*ctx
.bumps
.get("flash_account")
.ok_or(UnstakeError::PdaBumpNotCached)?],
];
allocate_assign_pda(AllocateAssignPdaArgs {
system_program,
pda_account: flash_account,
pda_account_owner_program: &crate::ID,
pda_account_len: FlashAccount::account_len(),
pda_account_signer_seeds: &[flash_account_seeds],
})?;
if flash_account.lamports() == 0 {
transfer(
CpiContext::new_with_signer(
system_program.to_account_info(),
Transfer {
from: pool_sol_reserves.to_account_info(),
to: flash_account.to_account_info(),
},
&[seeds],
),
1, // 1 lamport hot potato
)?;
}

// increment and save flash_account
Expand Down
11 changes: 11 additions & 0 deletions programs/unstake/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ declare_id!("6KBz9djJAH3gRHscq9ujMpyZ5bCK9a27o3ybDtJLXowz");
#[cfg(not(feature = "local-testing"))]
declare_id!("unpXTU2Ndrc7WWNyEhQWe4udTzSibLPi25SXv2xbCHQ");

#[cfg(not(feature = "no-entrypoint"))]
solana_security_txt::security_txt! {
name: "Sanctum Unstake Program",
project_url: "https://sanctum.so",
contacts: "telegram:gnaynaud,telegram:f812_socean,telegram:fpsocean",
policy: "https://github.com/igneous-labs/sanctum-unstake-program/blob/master/SECURITY.md",
preferred_languages: "en",
source_code: "https://github.com/igneous-labs/sanctum-unstake-program",
auditors: "Sec3"
}

pub mod anchor_len;
pub mod consts;
pub mod errors;
Expand Down
90 changes: 69 additions & 21 deletions tests/test-unstake-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1742,15 +1742,8 @@ describe("internals", () => {
expect(flashAccountInfo).to.be.null;
});

it("it take flash loan twice in same tx", async () => {
it("it fails to take flash loan twice in same tx", async () => {
const loanAmt = new BN(1_000_000_000);
const [protocolFeeDestBalancePre, poolSolReservesBalancePre] =
await Promise.all(
[protocolFeeDestination, poolSolReserves].map((pk) =>
program.provider.connection.getBalance(pk)
)
);

const takeIx = await program.methods
.takeFlashLoan(loanAmt)
.accounts({
Expand Down Expand Up @@ -1785,24 +1778,79 @@ describe("internals", () => {
const signature = await program.provider.connection.sendTransaction(tx, {
skipPreflight: true,
});
await program.provider.connection.confirmTransaction({
const {
value: { err },
} = await program.provider.connection.confirmTransaction({
signature,
...bh,
});

const [protocolFeeDestBalancePost, poolSolReservesBalancePost] =
await Promise.all(
[protocolFeeDestination, poolSolReserves].map((pk) =>
program.provider.connection.getBalance(pk)
)
);
const flashAccountInfo = await program.provider.connection.getAccountInfo(
flashAccount
expect(err).to.satisfy(
checkAnchorError(
6015,
"Flash loan active, no further flash loans and liquidity addition allowed"
)
);
});

expect(protocolFeeDestBalancePost).to.be.gt(protocolFeeDestBalancePre);
expect(poolSolReservesBalancePost).to.be.gt(poolSolReservesBalancePre);
expect(flashAccountInfo).to.be.null;
it("it fails to take flash loan then add liquidity", async () => {
const loanAmt = new BN(1_000_000_000);
const takeIx = await program.methods
.takeFlashLoan(loanAmt)
.accounts({
receiver: flashLoaner.publicKey,
poolAccount: poolKeypair.publicKey,
poolSolReserves,
flashAccount,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
const addLiquidityIx = await program.methods
.addLiquidity(loanAmt)
.accounts({
from: flashLoaner.publicKey,
poolAccount: poolKeypair.publicKey,
poolSolReserves,
lpMint: lpMintKeypair.publicKey,
mintLpTokensTo: lperAta,
flashAccount,
})
.instruction();
const repayIx = await program.methods
.repayFlashLoan()
.accounts({
repayer: flashLoaner.publicKey,
poolAccount: poolKeypair.publicKey,
poolSolReserves,
flashAccount,
flashLoanFeeAccount,
protocolFeeAccount: protocolFeeAddr,
protocolFeeDestination,
})
.instruction();
const bh = await program.provider.connection.getLatestBlockhash();
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: flashLoaner.publicKey,
recentBlockhash: bh.blockhash,
instructions: [takeIx, addLiquidityIx, repayIx],
}).compileToV0Message()
);
tx.sign([flashLoaner]);
const signature = await program.provider.connection.sendTransaction(tx, {
skipPreflight: true,
});
const {
value: { err },
} = await program.provider.connection.confirmTransaction({
signature,
...bh,
});
expect(err).to.satisfy(
checkAnchorError(
6015,
"Flash loan active, no further flash loans and liquidity addition allowed"
)
);
});
});
});
4 changes: 4 additions & 0 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export function checkAnchorError(
if (err.code != undefined) {
// first error type
return err.code === errorCode && err.msg === errorMessage;
} else if (err.InstructionError !== undefined) {
// confirmTransaction result
const [_ixIndex, errObj] = err.InstructionError;
return errObj.Custom === errorCode;
} else {
// second error type
return (
Expand Down