20,000 USDC
View results
Submission Details
Severity: low

`Lender.buyLoan` and `Lender.giveLoan` does not transfer the `newPool.auctionLength` to the Loan

Summary

When a Loan is transferred to a new pool, the loan.auctionLength is not updated to the new pool auctionLength value

Vulnerability Details

In the giveLoan function, a loan can be transferred to a new pool only if the terms of the pool are more favorable to the borrower. These conditions must be me (on top of other requirements)

  • newPool.interestRate <= loan.interestRate

  • newPool.auctionLength >= loan.auctionLength

In the buyLoan the scenario is different because you can buy a loan only if that loan has been actioned and at that point the borrower is forced to accept the new pool's terms. There are still some requirements to be matched, but the main one is just that newPool.interestRate > currentAuctionRate.

In both scenarios, the properties interestRate and auctionLength should be updated to follow the pool's terms, but only the interestRate is really updated.

Because of this, the loan auctionLength will always be equal to the starting pool's terms and is updated only if the loan is refinanced.

Impact

The loan will always use the original pool auctionLength while instead should use the new pool auctionLength.

This will impact the lender that wants to start an auction and the lenders that will want to buy that auction because they are forced to obey to the original (the very first) pool's auctionLength.

Tools Used

Manual review + foundry test

BaseLender.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "TERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract WrappedLender is Lender {
function getLoanDebtDetail(uint256 loanId) external view returns (uint256 fullDebt, uint256 interest, uint256 fees) {
Loan memory loan = loans[loanId];
// calculate the accrued interest
(interest, fees) = _calculateInterest(loan);
fullDebt = loan.debt + interest + fees;
}
function getPoolInfo(bytes32 poolId) external view returns (Pool memory) {
return pools[poolId];
}
function getLoanInfo(uint256 loanId) external view returns (Loan memory) {
return loans[loanId];
}
}
contract BaseLender is Test {
WrappedLender public lender;
TERC20 public loanToken;
TERC20 public collateralToken;
address public lender1 = address(0x1);
address public lender2 = address(0x2);
address public borrower = address(0x3);
address public fees = address(0x4);
function setUp() public virtual {
lender = new WrappedLender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(lender1), 100000 ether);
loanToken.mint(address(lender2), 100000 ether);
collateralToken.mint(address(borrower), 100000 ether);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
vm.startPrank(lender2);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
}
function borrow(address _borrower, bytes32 poolId) public {
vm.startPrank(_borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100 ether,
collateral: 100 ether
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
vm.stopPrank();
}
function createPool(address _lender) public returns (bytes32){
vm.startPrank(_lender);
Pool memory p = Pool({
lender: _lender,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 ether,
poolBalance: 1000 ether,
maxLoanRatio: 2 ether,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
vm.stopPrank();
return poolId;
}
function startAuction(address _lender, uint256 loanId) public {
uint256[] memory loans = new uint256[](1);
loans[0] = loanId;
vm.prank(_lender);
lender.startAuction(loans);
}
function seizeLoan(uint256 loanId) public {
uint256[] memory loans = new uint256[](1);
loans[0] = loanId;
lender.seizeLoan(loans);
}
function getPoolId(address _lender) public returns (bytes32) {
return keccak256(
abi.encode(
address(_lender),
address(loanToken),
address(collateralToken)
)
);
}
}

AuctionLengthNotUpdated.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./BaseLender.sol";
contract AuctionLengthNotUpdatedTest is BaseLender {
function setUp() override public {
super.setUp();
}
function setupAndBorrow() private returns (bytes32, bytes32, uint256) {
// Lender1 first pool with auctionLength equal to 1 days
vm.prank(lender1);
bytes32 poolId1 = lender.setPool(Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 ether,
poolBalance: 1000 ether,
maxLoanRatio: 2 ether,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
}));
// Lender2 first pool with auctionLength equal to 2 days
vm.prank(lender2);
bytes32 poolId2 = lender.setPool(Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 ether,
poolBalance: 1000 ether,
maxLoanRatio: 2 ether,
auctionLength: 2 days,
interestRate: 1000,
outstandingLoans: 0
}));
// Borrower borrows from first pool
borrow(borrower, poolId1);
assertEq(lender.getPoolInfo(poolId1).auctionLength, 1 days);
assertEq(lender.getPoolInfo(poolId2).auctionLength, 2 days);
// loan has the same auctionLength of the first pool
assertEq(lender.getLoanInfo(0).auctionLength, lender.getPoolInfo(poolId1).auctionLength);
return (poolId1, poolId2, 0);
}
function testAuctionLengthNotUpdatedByGiveLoan() public {
(bytes32 poolId1, bytes32 poolId2, uint256 loanId) = setupAndBorrow();
// Lender1 gives the loan to Lender2 pool
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = loanId;
bytes32[] memory poolIds = new bytes32[](1);
poolIds[0] = poolId2;
vm.prank(lender1);
lender.giveLoan(loanIds, poolIds);
// The loan auctionLength should be equal to the new pool auctionLength but it's not like that
// the loan.auctionLength should be equal to 2 days but instead is equal to 1 days
assertEq(lender.getLoanInfo(loanId).auctionLength, lender.getPoolInfo(poolId1).auctionLength);
assertFalse(lender.getLoanInfo(loanId).auctionLength == lender.getPoolInfo(poolId2).auctionLength);
}
function testAuctionLengthNotUpdatedByBuyLoan() public {
(bytes32 poolId1, bytes32 poolId2, uint256 loanId) = setupAndBorrow();
// Lender1 gives the loan to Lender2 pool
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = loanId;
// Lender1 start the auction
vm.prank(lender1);
lender.startAuction(loanIds);
// wait a bit to avoid RateTooHigh
vm.warp(block.timestamp + 12 hours);
// Lender2 buy the loan
vm.prank(lender2);
lender.buyLoan(loanId, poolId2);
// The loan auctionLength should be equal to the new pool auctionLength but it's not like that
// the loan.auctionLength should be equal to 2 days but instead is equal to 1 days
assertEq(lender.getLoanInfo(loanId).auctionLength, lender.getPoolInfo(poolId1).auctionLength);
assertFalse(lender.getLoanInfo(loanId).auctionLength == lender.getPoolInfo(poolId2).auctionLength);
}
}

Recommendations

On both buyLoan and giveLoan the loans[loanId].auctionLength should be updated to newPool.auctionLength

Support

FAQs

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

Give us feedback!