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

Incorrect loan ratio calculation

Summary

a. The math for calculating the loanRatio allows for precision loss in favour of the borrower.

b. Another issue with it is that the hard coded decimal of 10 ** 18 could cause the lending protocol to require more or less collateral than intended.

Vulnerability Details

a. The EVM does not allow for floating point values so all divisions are rounded down. The math for calculating the loanRatio essentially boils down to debt / collateral. We can say the multiplication by 10 ** 18 is there to ensure that the result retains the correct decimal after division.


Given the premise above, let's walk through an example without the multiplication by 10 ** 18. Say the maxLoanRatio = 2, and a borrower wants to get debt = 100 with collateral = 49. The loanRatio is 100 / 49 = 2.0408 which is above the maxLoanRatio of 2 and should fail. It will however pass because solidity will round it down to 2.


b. By assuming that the decimal for the collateral token is always going to be 10 ** 18, if the actual decimal is more e.g 10 ** 20, it allows the borrower to deposit less collateral than expected by the lender, potentially beating the maxLoanRation and vice versa

Impact

a. The loss of precision allows for under collateralised loans.

b. Borrower can use less collateral than what's provided in the maxLoanRatio or borrowers would be required to pay more collateral than lender expected or borrowers would only be able to get less loan than the lender would have expected.

POC (a)

function test_borrow() public {
uint256 debtAmount = 100_000_000_000_000_000_000;
uint256 collateralAmount = 49_999_999_999_999_999_999;
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: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: debtAmount,
collateral: collateralAmount
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
// the test will fail because it won't revert
vm.expectRevert(RatioTooHigh.selector);
lender.borrow(borrows);
}

POC (b)

None. it's simple enough.

Tools Used

Foundry

Recommendations

A better way to do the comparison would be to replace all occurrences of

uint256 loanRatio = (debt * 10 ** 18) / collateral;
if (loanRatio > pool.maxLoanRatio) revert RatioTooHigh();

with

uint loanTokenBasis = 10 ** IERC20(pool.loanToken).decimals();
uint collateralTokenBasis = 10 ** IERC20(pool.collateralToken).decimals();
uint maxAllowableDebt = (pool.maxLoanRatio * collateral * loanTokenBasis ) /
10 ** 18 / collateralTokenBasis;
if (debt > maxAllowableDebt) revert RatioTooHigh();

It has the advantage of having divisions work in favour of the lender and handles basis conversion when both tokens have different decimals. The borrower must have enough collateral before they pass that check.

Support

FAQs

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