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

Wrong Calculation When Using Fee-on-transfer Tokens

Summary

The vulnerability stems from the Lender.sol contract's assumption that all ERC20 tokens behave uniformly when transferred. Yet, fee-on-transfer tokens, with transfer taxes, deviate from this behavior, causing discrepancies between sent and received amounts. Using these tokens in the protocol triggers issues, leading to overestimation of a user's debt during borrowing. This discrepancy obstructs loan repayment and collateral retrieval.

Vulnerability Details

The vulnerability arises from the flawed assumption within the Lender.sol contract that all ERC20 tokens adhere uniformly to transfers. However, tokens with fee-on-transfer mechanisms, such as those involving transfer taxes, exhibit distinct behavior. The tokens' sender-sent amount doesn't match the destination-received amount, leading to discrepancies.

The issue is exacerbated when employing transfer-on-fee tokens within the protocol. This scenario distorts the user's debt calculation during loan acquisition. Consequently, borrowers may struggle to repay loans and retrieve their collateral accurately.

Impact

This vulnerability significantly skews debt calculations in the protocol. Consequently, borrowers face challenges repaying loans and regaining collateral. Consider a hypothetical instance: a pool accommodating USDT collateral with a 1% transfer tax. Borrowing $500 ETH requires sending $1,000 USDT. Though Alice forwards $1,000, transaction fees reduce this to $990, received by the protocol. Subsequently, when Alice repays the $500 ETH via the repay() function, it tries to transfer $1,000 USDT. However, since Alice's collateral doesn't cover this amount, the transaction may fail (note: this failure might not be detected due to unchecked transfer and transferFrom return values).

Tools Used

Manual Review

Recommendations

To rectify this, when calculating collateral received by the protocol, ascertain the balance of the Lender before and after receiving the collateral. Employ the difference between these balances to determine the accurate collateral amount.

As an illustration, adjust the borrow() function as demonstrated below:

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;
// get the pool info
Pool memory pool = pools[poolId];
// make sure the pool exists
if (pool.lender == address(0)) revert PoolConfig();
// validate the loan
if (debt < pool.minLoanSize) revert LoanTooSmall();
if (debt > pool.poolBalance) revert LoanTooLarge();
if (collateral == 0) revert ZeroCollateral();
///// First calculate the balance of this contract
uint256 balanceBefore = IERC20(loan.loanToken).balanceOf(address(this));
///// Then send the collateral
// transfer the collateral tokens from the borrower to the contract
IERC20(loan.collateralToken).transferFrom(
msg.sender,
address(this),
collateral
);
///// Then calculate the balance to find the received amount
collateral = IERC20(loan.loanToken).balanceOf(address(this)) - balanceBefore;
// make sure the user isn't borrowing too much
uint256 loanRatio = (debt * 10 ** 18) / collateral;
if (loanRatio > pool.maxLoanRatio) revert RatioTooHigh();
// create the loan
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
});
// update the pool balance
_updatePoolBalance(poolId, pools[poolId].poolBalance - debt);
pools[poolId].outstandingLoans += debt;
// calculate the fees
uint256 fees = (debt * borrowerFee) / 10000;
// transfer fees
IERC20(loan.loanToken).transfer(feeReceiver, fees);
// transfer the loan tokens from the pool to the borrower
IERC20(loan.loanToken).transfer(msg.sender, debt - fees);
loans.push(loan);
emit Borrowed(
msg.sender,
pool.lender,
loans.length - 1,
debt,
collateral,
pool.interestRate,
block.timestamp
);
}
}

Support

FAQs

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