sherlock-audit / 2024-04-teller-finance-judging

10 stars 9 forks source link

0xadrii - Not considering `liquidityThresholdPercent` will make pool utilization ratio be wrongly computed #232

Closed sherlock-admin4 closed 4 months ago

sherlock-admin4 commented 4 months ago

0xadrii

medium

Not considering liquidityThresholdPercent will make pool utilization ratio be wrongly computed

Summary

The pool’s utilization ratio does not consider the liquidityThresholdPercent . This will make users capable of paying loans with a smaller interest rate than the expected one.

Vulnerability Detail

When the acceptFundsForAcceptBid function in the lender commitment contract is called, a minimum interest rate will be enforced to match the current market demand:

// LenderCommitmentGroup_Smart.sol

function acceptFundsForAcceptBid(
        address _borrower,
        uint256 _bidId,
        uint256 _principalAmount,
        uint256 _collateralAmount,
        address _collateralTokenAddress,
        uint256 _collateralTokenId, 
        uint32 _loanDuration,
        uint16 _interestRate
    ) external onlySmartCommitmentForwarder whenNotPaused {
        ...
        //the interest rate must be at least as high has the commitment demands. The borrower can use a higher interest rate although that would not be beneficial to the borrower.
        require(_interestRate >= getMinInterestRate(), "Invalid interest rate");

The minimum interest rate will be obtained from the getMinInterestRate function, which will perform the following computation to check the interest rate:

// LenderCommitmentGroup_Smart.sol 
function getMinInterestRate() public view returns (uint16) {
    return interestRateLowerBound + uint16( uint256(interestRateUpperBound-interestRateLowerBound).percent(getPoolUtilizationRatio()) );
}

As we can see, the current pool utilization ratio will serve as a percentage applied to the difference between interestRateUpperBound and interestRateLowerBound. For example, if interestRateLowerBound is 15%, interestRateUpperBound is 25% and getPoolUtilizationRatio returns 80%, then the minimum interest rate to pay will be 15% + (10% * 80%) = 23%.

The utilization ratio is then obtained in the following way:

// LenderCommitmentGroup_Smart.sol 
function getPoolUtilizationRatio() public view returns (uint16) {

        if (getPoolTotalEstimatedValue() == 0) {
            return 0; 
        }

        return uint16( 
            Math.min( 
                getTotalPrincipalTokensOutstandingInActiveLoans()  * 10000  
                / getPoolTotalEstimatedValue(), 
                10000  
            )
        );
    }   

The idea is to check how many of the borrowable funds have actually been borrowed, and extract a ratio to see the total utilization of the pool. To achieve this, getTotalPrincipalTokensOutstandingInActiveLoans is divided by getPoolTotalEstimatedValue.

This calculation is wrong because getPoolTotalEstimatedValue returns the total amount of assets in the pool without considering the liquidityThresholdPercent parameter.

The liquidityThresholdPercent is a pool configuration used to limit the amount of funds that can be borrowed in the pool. When set to 100% , the entire pool can be drawn for lending. When set to 80% instead, only 80% of the pool can be drawn for lending.

This is not considered in the minimum interest rate calculation, which leads to computations always being performed with the entire amount of assets held in the pool (essentially, not considering this value is the same as assuming that the liquidityThresholdPercent is set to 100%).

The attached proof of concept shows how this issue allows users to borrow funds at a lower interest rate than the expected.

Proof of Concept

Consider the following scenario, where a new pool has been created with the following configuration parameters:

Let’s say that getTotalPrincipalTokensOutstandingInActiveLoans (the total amount borrowed) returns 100, and let’s also say that the total amount of pooled funds is 300.

With the current code, the minimum interest computation would be given by:

utilization ratio = 100/300 = 33.333…%

minimum interest rate = 15% + ((25% - 15%) * (33,333%)) = 18.333…% approximately

However, considering the liquidityThresholdPercent configuration, the following calculation should be applied:

utilization ratio = 100 / (300 * 75%) = 60.06…% —> (The utilization ratio increases, as we can’t borrow the totality of the pool)

minimum interest rate = 15% + ((25% - 15%) * (60.06%)) = 21.006…% approximately

As we can see, the minimum interest rate that should be paid changes when the liquidity threshold is actually considered, with the current approach allowing borrowers to take loans with a lower interest rate.

getPoolTotalEstimatedValue() )).percent(liquidityThresholdPercent)

Impact

Medium. Borrowers will always be able to pay a smaller interest rate than the expected one. This will lead to lenders obtaining less funds.

Code Snippet

https://github.com/sherlock-audit/2024-04-teller-finance/blob/main/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol#L765

Tool used

Manual Review

Recommendation

Consider the liquidityThresholdPercent when computing the pool’s utilization:

// LenderCommitmentGroup_Smart.sol 

function getPoolUtilizationRatio() public view returns (uint16) {

        if (getPoolTotalEstimatedValue() == 0) {
            return 0; 
        }

        return uint16( 
            Math.min( 
                getTotalPrincipalTokensOutstandingInActiveLoans()  * 10000  
-                / getPoolTotalEstimatedValue(), 
+                / getPoolTotalEstimatedValue().percent(liquidityThresholdPercent), 
                10000  
            )
        );
    }  

Duplicate of #70