Summary
In the protocol,the Debt and Collateral token amounts rather than value is considered. This breaks the protocol.
Vulnerability Details
The lender fixes the loanToken and collateralToken of choice in the setting up of the pool.
The amount of loanTokens to be loaned are transferred by the lender to the pool contract via function setPool.
function setPool(Pool calldata p) public returns (bytes32 poolId) {
if (
p.lender != msg.sender ||
p.minLoanSize == 0 ||
p.maxLoanRatio == 0 ||
p.auctionLength == 0 ||
p.auctionLength > MAX_AUCTION_LENGTH ||
p.interestRate > MAX_INTEREST_RATE
) revert PoolConfig();
poolId = getPoolId(p.lender, p.loanToken, p.collateralToken);
if (p.outstandingLoans != pools[poolId].outstandingLoans)
revert PoolConfig();
uint256 currentBalance = pools[poolId].poolBalance;
if (p.poolBalance > currentBalance) {
IERC20(p.loanToken).transferFrom(
p.lender,
address(this),
p.poolBalance - currentBalance
);
} else if (p.poolBalance < currentBalance) {
IERC20(p.loanToken).transfer(
p.lender,
currentBalance - p.poolBalance
);
}
https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L130C2-L163
Borrowers can specify the amount to borrow and collateral tokens to deposit. However, the protocol does not convert the tokens to the relative value of the debt tokens in the accounting methodology.
function borrow(Borrow[] calldata borrows) public {
for (uint256 i = 0; i < borrows.length; i++) {
bytes32 poolId = borrows[i].poolId;
uint256 debt = borrows[i].debt;
uint256 collateral = borrows[i].collateral;
Pool memory pool = pools[poolId];
if (pool.lender == address(0)) revert PoolConfig();
if (debt < pool.minLoanSize) revert LoanTooSmall();
if (debt > pool.poolBalance) revert LoanTooLarge();
if (collateral == 0) revert ZeroCollateral();
uint256 loanRatio = (debt * 10 ** 18) / collateral;
if (loanRatio > pool.maxLoanRatio) revert RatioTooHigh();
Loan memory loan = Loan({
lender: pool.lender,
borrower: msg.sender,
loanToken: pool.loanToken,
collateralToken: pool.collateralToken,
debt: debt,
collateral: collateral,
interestRate: pool.interestRate,
startTimestamp: block.timestamp,
auctionStartTimestamp: type(uint256).max,
auctionLength: pool.auctionLength
});
_updatePoolBalance(poolId, pools[poolId].poolBalance - debt);
pools[poolId].outstandingLoans += debt;
uint256 fees = (debt * borrowerFee) / 10000;
IERC20(loan.loanToken).transfer(feeReceiver, fees);
IERC20(loan.loanToken).transfer(msg.sender, debt - fees);
IERC20(loan.collateralToken).transferFrom(
msg.sender,
address(this),
collateral
);
loans.push(loan);
emit Borrowed(
msg.sender,
pool.lender,
loans.length - 1,
debt,
collateral,
pool.interestRate,
block.timestamp
);
}
}
https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L232C3-L287C6
Consider the scenario:
loanToken = USDC
collateralToken = ETH
This means that for a collaterization ratio of 200% (LTV = 0.5), borrowers need to deposit 2 ETH in order to borrow 1 USDC.
On the flipside, where:
loanToken = ETH
collateralToken = USDC
This means that for a collaterization ratio of 200% (LTV = 0.5), borrowers only need to deposit 2 USDC in order to borrow 1 ETH.
This breaks the functionality of the protocol.
The only time the current accounting would work is if the loanToken and collateralToken is the same - it does not make business sense to set up such a pool.
Impact
See above.
Tools Used
Manual review.
Recommendations
Accounting methodology should consider value of tokens rather than number of tokens.