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

Use do while loops instead of for loops.

Summary

You can use do while loops instead of for loops to save gas.

Vulnerability Details

A do while loop will cost less gas since the condition is not being checked for the first iteration. Also, using do while loop with ++i is more gas efficient and wrapping ++i with unchecked keyword makes the whole iteration the most gas efficient of all. Also, the i is not initialized to 0 and the length of loop is cached.

Savings

There are six instances of for loops in the Lender.sol contract.

  1. Savings for borrow():

Average Median Max
Before 306602 364364 364364
After 306513 364265 364265
  1. Savings for seizeLoan():

Average Median Max
Before 17001 2306 46392
After 16965 2284 46328
  1. Savings for startAuction():

Average Median Max
Before 5337 5733 5733
After 5246 5634 5634
  1. Savings for giveLoan():

Average Median Max
Before 35436 35436 35436
After 35356 35356 35356
  1. Savings for repay():

Average Median Max
Before 22065 22065 22865
After 22001 22001 22801
  1. Savings for refinance():

Average Median Max
Before 21076 21076 37164
After 21026 21026 37085

Tools Used

Manual Review and gas report via $ forge test --gas-report

Recommendations

  1. For borrow() change from:

https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L232C2-L287C6

To:

/src/Lender.sol
function borrow(Borrow[] calldata borrows) public {
uint256 i;
uint256 length = borrows.length;
do {
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();
// 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);
// transfer the collateral tokens from the borrower to the contract
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
);
unchecked {
++i;
}
} while (i < length);
}
  1. For seizeLoan() change from:

https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L548-L586

To:

/src/Lender.sol
function seizeLoan(uint256[] calldata loanIds) public {
uint256 i;
uint256 length = loanIds.length;
do {
uint256 loanId = loanIds[i];
// get the loan info
Loan memory loan = loans[loanId];
// validate the loan
if (loan.auctionStartTimestamp == type(uint256).max) {
revert AuctionNotStarted();
}
if (block.timestamp < loan.auctionStartTimestamp + loan.auctionLength) revert AuctionNotEnded();
// calculate the fee
uint256 govFee = (borrowerFee * loan.collateral) / 10000;
// transfer the protocol fee to governance
IERC20(loan.collateralToken).transfer(feeReceiver, govFee);
// transfer the collateral tokens from the contract to the lender
IERC20(loan.collateralToken).transfer(loan.lender, loan.collateral - govFee);
bytes32 poolId = keccak256(abi.encode(loan.lender, loan.loanToken, loan.collateralToken));
// update the pool outstanding loans
pools[poolId].outstandingLoans -= loan.debt;
emit LoanSiezed(loan.borrower, loan.lender, loanId, loan.collateral);
// delete the loan
delete loans[loanId];
unchecked {
++i;
}
} while (i < length);
}
  1. For startAuction() change from:

https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L437C1-L460C1

To:

/src/Lender.sol
function startAuction(uint256[] calldata loanIds) public {
uint256 i;
uint256 length = loanIds.length;
do {
uint256 loanId = loanIds[i];
// get the loan info
Loan memory loan = loans[loanId];
// validate the loan
if (msg.sender != loan.lender) revert Unauthorized();
if (loan.auctionStartTimestamp != type(uint256).max) {
revert AuctionStarted();
}
// set the auction start timestamp
loans[loanId].auctionStartTimestamp = block.timestamp;
emit AuctionStart(
loan.borrower, loan.lender, loanId, loan.debt, loan.collateral, block.timestamp, loan.auctionLength
);
unchecked {
++i;
}
} while (i < length);
}
  1. For giveLoan() change from:

https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L355C1-L433C1

To:

/src/Lender.sol
function giveLoan(uint256[] calldata loanIds, bytes32[] calldata poolIds) external {
uint256 i;
uint256 length = loanIds.length;
do {
uint256 loanId = loanIds[i];
bytes32 poolId = poolIds[i];
// get the loan info
Loan memory loan = loans[loanId];
// validate the loan
if (msg.sender != loan.lender) revert Unauthorized();
// get the pool info
Pool memory pool = pools[poolId];
// validate the new loan
if (pool.loanToken != loan.loanToken) revert TokenMismatch();
if (pool.collateralToken != loan.collateralToken) {
revert TokenMismatch();
}
// new interest rate cannot be higher than old interest rate
if (pool.interestRate > loan.interestRate) revert RateTooHigh();
// auction length cannot be shorter than old auction length
if (pool.auctionLength < loan.auctionLength) revert AuctionTooShort();
// calculate the interest
(uint256 lenderInterest, uint256 protocolInterest) = _calculateInterest(loan);
uint256 totalDebt = loan.debt + lenderInterest + protocolInterest;
if (pool.poolBalance < totalDebt) revert PoolTooSmall();
if (totalDebt < pool.minLoanSize) revert LoanTooSmall();
uint256 loanRatio = (totalDebt * 10 ** 18) / loan.collateral;
if (loanRatio > pool.maxLoanRatio) revert RatioTooHigh();
// update the pool balance of the new lender
_updatePoolBalance(poolId, pool.poolBalance - totalDebt);
pools[poolId].outstandingLoans += totalDebt;
// update the pool balance of the old lender
bytes32 oldPoolId = getPoolId(loan.lender, loan.loanToken, loan.collateralToken);
_updatePoolBalance(oldPoolId, pools[oldPoolId].poolBalance + loan.debt + lenderInterest);
pools[oldPoolId].outstandingLoans -= loan.debt;
// transfer the protocol fee to the governance
IERC20(loan.loanToken).transfer(feeReceiver, protocolInterest);
emit Repaid(
loan.borrower,
loan.lender,
loanId,
loan.debt + lenderInterest + protocolInterest,
loan.collateral,
loan.interestRate,
loan.startTimestamp
);
// update the loan with the new info
loans[loanId].lender = pool.lender;
loans[loanId].interestRate = pool.interestRate;
loans[loanId].startTimestamp = block.timestamp;
loans[loanId].auctionStartTimestamp = type(uint256).max;
loans[loanId].debt = totalDebt;
emit Borrowed(
loan.borrower,
pool.lender,
loanId,
loans[loanId].debt,
loans[loanId].collateral,
pool.interestRate,
block.timestamp
);
unchecked {
++i;
}
} while (i < length);
}
  1. For repay() change from:

https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L292C1-L345C6
To:

/src/Lender.sol
function repay(uint256[] calldata loanIds) public {
uint256 i;
uint256 length = loanIds.length;
do {
uint256 loanId = loanIds[i];
// get the loan info
Loan memory loan = loans[loanId];
// calculate the interest
(uint256 lenderInterest, uint256 protocolInterest) = _calculateInterest(loan);
bytes32 poolId = getPoolId(loan.lender, loan.loanToken, loan.collateralToken);
// update the pool balance
_updatePoolBalance(poolId, pools[poolId].poolBalance + loan.debt + lenderInterest);
pools[poolId].outstandingLoans -= loan.debt;
// transfer the loan tokens from the borrower to the pool
IERC20(loan.loanToken).transferFrom(msg.sender, address(this), loan.debt + lenderInterest);
// transfer the protocol fee to the fee receiver
IERC20(loan.loanToken).transferFrom(msg.sender, feeReceiver, protocolInterest);
// transfer the collateral tokens from the contract to the borrower
IERC20(loan.collateralToken).transfer(loan.borrower, loan.collateral);
emit Repaid(
msg.sender, loan.lender, loanId, loan.debt, loan.collateral, loan.interestRate, loan.startTimestamp
);
// delete the loan
delete loans[loanId];
unchecked {
++i;
}
} while (i < length);
}
  1. For refinance() change from:

https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L591C3-L710C6

To:

/src/Lender.sol
function refinance(Refinance[] calldata refinances) public {
uint256 i;
uint256 length = refinances.length;
do {
uint256 loanId = refinances[i].loanId;
bytes32 poolId = refinances[i].poolId;
bytes32 oldPoolId =
keccak256(abi.encode(loans[loanId].lender, loans[loanId].loanToken, loans[loanId].collateralToken));
uint256 debt = refinances[i].debt;
uint256 collateral = refinances[i].collateral;
// get the loan info
Loan memory loan = loans[loanId];
// validate the loan
if (msg.sender != loan.borrower) revert Unauthorized();
// get the pool info
Pool memory pool = pools[poolId];
// validate the new loan
if (pool.loanToken != loan.loanToken) revert TokenMismatch();
if (pool.collateralToken != loan.collateralToken) {
revert TokenMismatch();
}
if (pool.poolBalance < debt) revert LoanTooLarge();
if (debt < pool.minLoanSize) revert LoanTooSmall();
uint256 loanRatio = (debt * 10 ** 18) / collateral;
if (loanRatio > pool.maxLoanRatio) revert RatioTooHigh();
// calculate the interest
(uint256 lenderInterest, uint256 protocolInterest) = _calculateInterest(loan);
uint256 debtToPay = loan.debt + lenderInterest + protocolInterest;
// update the old lenders pool
_updatePoolBalance(oldPoolId, pools[oldPoolId].poolBalance + loan.debt + lenderInterest);
pools[oldPoolId].outstandingLoans -= loan.debt;
// now lets deduct our tokens from the new pool
_updatePoolBalance(poolId, pools[poolId].poolBalance - debt);
pools[poolId].outstandingLoans += debt;
if (debtToPay > debt) {
// we owe more in debt so we need the borrower to give us more loan tokens
// transfer the loan tokens from the borrower to the contract
IERC20(loan.loanToken).transferFrom(msg.sender, address(this), debtToPay - debt);
} else if (debtToPay < debt) {
// we have excess loan tokens so we give some back to the borrower
// first we take our borrower fee
uint256 fee = (borrowerFee * (debt - debtToPay)) / 10000;
IERC20(loan.loanToken).transfer(feeReceiver, fee);
// transfer the loan tokens from the contract to the borrower
IERC20(loan.loanToken).transfer(msg.sender, debt - debtToPay - fee);
}
// transfer the protocol fee to governance
IERC20(loan.loanToken).transfer(feeReceiver, protocolInterest);
// update loan debt
loans[loanId].debt = debt;
// update loan collateral
if (collateral > loan.collateral) {
// transfer the collateral tokens from the borrower to the contract
IERC20(loan.collateralToken).transferFrom(msg.sender, address(this), collateral - loan.collateral);
} else if (collateral < loan.collateral) {
// transfer the collateral tokens from the contract to the borrower
IERC20(loan.collateralToken).transfer(msg.sender, loan.collateral - collateral);
}
emit Repaid(msg.sender, loan.lender, loanId, debt, collateral, loan.interestRate, loan.startTimestamp);
loans[loanId].collateral = collateral;
// update loan interest rate
loans[loanId].interestRate = pool.interestRate;
// update loan start timestamp
loans[loanId].startTimestamp = block.timestamp;
// update loan auction start timestamp
loans[loanId].auctionStartTimestamp = type(uint256).max;
// update loan auction length
loans[loanId].auctionLength = pool.auctionLength;
// update loan lender
loans[loanId].lender = pool.lender;
// update pool balance
pools[poolId].poolBalance -= debt;
emit Borrowed(msg.sender, pool.lender, loanId, debt, collateral, pool.interestRate, block.timestamp);
emit Refinanced(loanId);
unchecked {
++i;
}
} while (i < length);
}

Support

FAQs

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

Give us feedback!