Summary
A pool that is created with tokens of different decimals won’t allow users to borrow.
Vulnerability Details
Proof of concept
POC would work in test.Lender.t.sol
function test_Wrong_LTV_Maths() public {
uint256 forkId = vm.createFork("https://eth-mainnet.g.alchemy.com/v2/NsixqnW3NZrQWYxL7uqxtv36t3t6LlfY");
vm.selectFork(forkId);
Lender beedleLend = new Lender();
address usdc = address(0x7EA2be2df7BA6E54B1A9C70676f668455E329d29);
address wbtc = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
vm.label(beedleLend.feeReceiver(), "Fee Receiver");
address _lender = makeAddr("_lender");
address _borrower = makeAddr("_borrower");
deal(usdc, _lender, (100_000*1e6));
deal(wbtc, _borrower, (50*1e8));
Pool memory poolParams = Pool(
_lender,
usdc,
wbtc,
uint256(10_000*1e6),
uint256(100_000*1e6),
uint256(0.5*1e18),
beedleLend.MAX_AUCTION_LENGTH(),
0,
uint256(0)
);
vm.startPrank(_lender);
IERC20(usdc).approve(address(beedleLend), type(uint256).max);
bytes32 poolId1 = beedleLend.setPool(poolParams);
beedleLend.updateInterestRate(poolId1, 1000);
vm.stopPrank();
vm.startPrank(_borrower);
Borrow[] memory borrows = new Borrow[](1);
IERC20(wbtc).approve(address(beedleLend), type(uint256).max);
borrows[0] = Borrow(poolId1,30_000*1e6,2*1e8);
vm.expectRevert();
beedleLend.borrow(borrows);
}
LTV is the ratio of the value of your loan to the value of your collateral.
Below is the formula used to calculate the Loan-to-Value ratio generally.
LTV = Loan Amount / Collateral Amount x 100%.
Beedle Lending pools use the same but with a twist of adding decimals formulae.
uint256 loanRatio = (debt * 10 ** 18) / collateral
In our case our lender creates an overcollaterized pool with a maximumLTV of 0.5, i.e with if 1 WBTC is 30k usd, the borrower must provide (2 * 30k usd) == 60k usd worth of USDC (maximum)
Using a WBTC/USDC pool from a mainnet fork in our poc , WBTC has a decimal 8. while usdc has a decimal of 6.
Lender creates a pool with setPool(poolParams), with 100_000*1e6 (100K usdc) poolBalance and minimum loan size of 10k usdc (10_000*1e6)
Borrower wants to borrow 30k usdc (30k usd)with 2 WBTC(60K usd) (overcollaterized postion), wont be able to because in the division, the numerator is always significantly higher (6 decimals * 1e18) and denominator is lower (8 decimals), which means LTV is always higher than usual from the borrowers parameters, which is not even scaled up in calculations, this would always fail as LTV would always be too high
Impact
Tools Used
Recommendations
using this scaled decimals formulae should work
@@ -244,6 +244,14 @@ contract Lender is Ownable {
if (collateral == 0) revert ZeroCollateral();
// make sure the user isn't borrowing too much
uint256 loanRatio = (debt * 10 ** 18) / collateral;
+ // changes
+ if (IERC20(pool.collateralToken).decimals() > IERC20(pool.loanToken).decimals()) {
+ loanRatio = (debt * 10 ** (IERC20(pool.collateralToken).decimals() - IERC20(pool.loanToken).decimals())/ collateral);
+ }
+ if (IERC20(pool.loanToken).decimals() > IERC20(pool.collateralToken).decimals()) {
+ loanRatio = (debt * 10 ** (IERC20(pool.loanToken).decimals() - IERC20(pool.collateralToken).decimals())/ collateral);
+ }
+ //changes
if (loanRatio > pool.maxLoanRatio) revert RatioTooHigh();
// create the loan
Loan memory loan = Loan({
@@ -10,4 +10,5 @@ interface IERC20 {
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
+ function decimals() external returns(uint256);
: