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.
Savings for borrow():
|
Average |
Median |
Max |
| Before |
306602 |
364364 |
364364 |
| After |
306513 |
364265 |
364265 |
Savings for seizeLoan():
|
Average |
Median |
Max |
| Before |
17001 |
2306 |
46392 |
| After |
16965 |
2284 |
46328 |
Savings for startAuction():
|
Average |
Median |
Max |
| Before |
5337 |
5733 |
5733 |
| After |
5246 |
5634 |
5634 |
Savings for giveLoan():
|
Average |
Median |
Max |
| Before |
35436 |
35436 |
35436 |
| After |
35356 |
35356 |
35356 |
Savings for repay():
|
Average |
Median |
Max |
| Before |
22065 |
22065 |
22865 |
| After |
22001 |
22001 |
22801 |
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
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;
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
);
unchecked {
++i;
}
} while (i < length);
}
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];
Loan memory loan = loans[loanId];
if (loan.auctionStartTimestamp == type(uint256).max) {
revert AuctionNotStarted();
}
if (block.timestamp < loan.auctionStartTimestamp + loan.auctionLength) revert AuctionNotEnded();
uint256 govFee = (borrowerFee * loan.collateral) / 10000;
IERC20(loan.collateralToken).transfer(feeReceiver, govFee);
IERC20(loan.collateralToken).transfer(loan.lender, loan.collateral - govFee);
bytes32 poolId = keccak256(abi.encode(loan.lender, loan.loanToken, loan.collateralToken));
pools[poolId].outstandingLoans -= loan.debt;
emit LoanSiezed(loan.borrower, loan.lender, loanId, loan.collateral);
delete loans[loanId];
unchecked {
++i;
}
} while (i < length);
}
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];
Loan memory loan = loans[loanId];
if (msg.sender != loan.lender) revert Unauthorized();
if (loan.auctionStartTimestamp != type(uint256).max) {
revert AuctionStarted();
}
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);
}
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];
Loan memory loan = loans[loanId];
if (msg.sender != loan.lender) revert Unauthorized();
Pool memory pool = pools[poolId];
if (pool.loanToken != loan.loanToken) revert TokenMismatch();
if (pool.collateralToken != loan.collateralToken) {
revert TokenMismatch();
}
if (pool.interestRate > loan.interestRate) revert RateTooHigh();
if (pool.auctionLength < loan.auctionLength) revert AuctionTooShort();
(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();
_updatePoolBalance(poolId, pool.poolBalance - totalDebt);
pools[poolId].outstandingLoans += totalDebt;
bytes32 oldPoolId = getPoolId(loan.lender, loan.loanToken, loan.collateralToken);
_updatePoolBalance(oldPoolId, pools[oldPoolId].poolBalance + loan.debt + lenderInterest);
pools[oldPoolId].outstandingLoans -= loan.debt;
IERC20(loan.loanToken).transfer(feeReceiver, protocolInterest);
emit Repaid(
loan.borrower,
loan.lender,
loanId,
loan.debt + lenderInterest + protocolInterest,
loan.collateral,
loan.interestRate,
loan.startTimestamp
);
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);
}
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];
Loan memory loan = loans[loanId];
(uint256 lenderInterest, uint256 protocolInterest) = _calculateInterest(loan);
bytes32 poolId = getPoolId(loan.lender, loan.loanToken, loan.collateralToken);
_updatePoolBalance(poolId, pools[poolId].poolBalance + loan.debt + lenderInterest);
pools[poolId].outstandingLoans -= loan.debt;
IERC20(loan.loanToken).transferFrom(msg.sender, address(this), loan.debt + lenderInterest);
IERC20(loan.loanToken).transferFrom(msg.sender, feeReceiver, protocolInterest);
IERC20(loan.collateralToken).transfer(loan.borrower, loan.collateral);
emit Repaid(
msg.sender, loan.lender, loanId, loan.debt, loan.collateral, loan.interestRate, loan.startTimestamp
);
delete loans[loanId];
unchecked {
++i;
}
} while (i < length);
}
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;
Loan memory loan = loans[loanId];
if (msg.sender != loan.borrower) revert Unauthorized();
Pool memory pool = pools[poolId];
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();
(uint256 lenderInterest, uint256 protocolInterest) = _calculateInterest(loan);
uint256 debtToPay = loan.debt + lenderInterest + protocolInterest;
_updatePoolBalance(oldPoolId, pools[oldPoolId].poolBalance + loan.debt + lenderInterest);
pools[oldPoolId].outstandingLoans -= loan.debt;
_updatePoolBalance(poolId, pools[poolId].poolBalance - debt);
pools[poolId].outstandingLoans += debt;
if (debtToPay > debt) {
IERC20(loan.loanToken).transferFrom(msg.sender, address(this), debtToPay - debt);
} else if (debtToPay < debt) {
uint256 fee = (borrowerFee * (debt - debtToPay)) / 10000;
IERC20(loan.loanToken).transfer(feeReceiver, fee);
IERC20(loan.loanToken).transfer(msg.sender, debt - debtToPay - fee);
}
IERC20(loan.loanToken).transfer(feeReceiver, protocolInterest);
loans[loanId].debt = debt;
if (collateral > loan.collateral) {
IERC20(loan.collateralToken).transferFrom(msg.sender, address(this), collateral - loan.collateral);
} else if (collateral < loan.collateral) {
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;
loans[loanId].interestRate = pool.interestRate;
loans[loanId].startTimestamp = block.timestamp;
loans[loanId].auctionStartTimestamp = type(uint256).max;
loans[loanId].auctionLength = pool.auctionLength;
loans[loanId].lender = pool.lender;
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);
}