| Issue | Instances | Total Gas Saved | |
|---|---|---|---|
| [G-01] | [G-01] Using storage instead of memory for structs/arrays saves gas | 3 | 6300 |
| [G-02] | Multiple accesses of a mapping/array should use a local variable cache | 28 | 1176 |
| [G-03] | Can Make The Variable Outside The Loop To Save Gas | 8 | 800 |
| [G-04] | Emitting storage values instead of the memory one | 4 | 400 |
| [G-05] | Cache state variables with stack variables | 2 | 200 |
| [G-06] | Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate | 1 | 21538 |
| [G-07] | Structs can be packed into fewer storage slots by truncating timestamp bytes | 1 | 2000 |
| [G-08] | ++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow, as is the case when used in for- and while-loops | 6 | 360 |
| [G-09] | Constructors can be marked payable | 4 | 84 |
| [G-10] | Avoid emitting block.timestamp in events | 5 | — |
Total: 62 instances over 10 issues with 33108 gas saved
When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct.
Gas Save 6300
117: Loan memory loan = loans[loanId];
```
```solidity
File: /src/Lender.sol
363: Loan memory loan = loans[loanId];
```
```solidity
File: /src/Lender.sol
367: Pool memory pool = pools[poolId];
```
Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times saves 42 gas per access due to not having to perform the same offset calculation every time. Help the Optimizer by saving a storage variable's reference instead of repeatedly fetching it
To help the optimizer,declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array. As an example, instead of repeatedly calling someMap[someIndex], save its reference like this: SomeStruct storage someStruct = someMap[someIndex] and use it.
Gas save 28*42 = 1176
```solidity
File: /src/Lender.sol
130: function setPool(Pool calldata p) public returns (bytes32 poolId) {
131: // validate the pool
....
141: // check if they already have a pool balance
142: poolId = getPoolId(p.lender, p.loanToken, p.collateralToken);
143:
144: // you can't change the outstanding loans
145: if (p.outstandingLoans != pools[poolId].outstandingLoans) //@audit first call
146: revert PoolConfig();
147:
148: uint256 currentBalance = pools[poolId].poolBalance; //@audit second call
....
166:
167: if (pools[poolId].lender == address(0)) { //@audit third call
168: // if the pool doesn't exist then create it
169: emit PoolCreated(poolId, p);
170: } else {
171: // if the pool does exist then update it
172: emit PoolUpdated(poolId, p);
173: }
174:
175: pools[poolId] = p;
176: }
```
```diff
File: /src/Lender.sol
130: function setPool(Pool calldata p) public returns (bytes32 poolId) {
131: // validate the pool
....
141: // check if they already have a pool balance
+ Pool calldata pool = pools[poolId];
142: poolId = getPoolId(p.lender, p.loanToken, p.collateralToken);
143:
144: // you can't change the outstanding loans
-145: if (p.outstandingLoans != pools[poolId].outstandingLoans)
+145: if (p.outstandingLoans != pool.outstandingLoans)
146: revert PoolConfig();
147:
-148: uint256 currentBalance = pools[poolId].poolBalance;
+148: uint256 currentBalance = pool.poolBalance;
....
166:
-167: if (pools[poolId].lender == address(0)) {
+167: if (pool.lender == address(0)) {
168: // if the pool doesn't exist then create it
169: emit PoolCreated(poolId, p);
170: } else {
171: // if the pool does exist then update it
172: emit PoolUpdated(poolId, p);
173: }
174:
175: pools[poolId] = p;
176: }
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L182C5-L192C6
```solidity
File: /src/Lender.sol
182: function addToPool(bytes32 poolId, uint256 amount) external {
183: if (pools[poolId].lender != msg.sender) revert Unauthorized(); //@audit first call
184: if (amount == 0) revert PoolConfig();
185: _updatePoolBalance(poolId, pools[poolId].poolBalance + amount); //@audit second call
186: // transfer the loan tokens from the lender to the contract
187: IERC20(pools[poolId].loanToken).transferFrom( //@audit third call
188: msg.sender,
189: address(this),
190: amount
191: );
192: }
```
```diff
File: /src/Lender.sol
182: function addToPool(bytes32 poolId, uint256 amount) external {
+ Pool calldata pool = pools[poolId];
-183: if (pools[poolId].lender != msg.sender) revert Unauthorized();
+183: if (pool.lender != msg.sender) revert Unauthorized();
184: if (amount == 0) revert PoolConfig();
-185: _updatePoolBalance(poolId, pools[poolId].poolBalance + amount);
+185: _updatePoolBalance(poolId, pool.poolBalance + amount);
186: // transfer the loan tokens from the lender to the contract
-187: IERC20(pools[poolId].loanToken).transferFrom(
+187: IERC20(pool.loanToken).transferFrom(
188: msg.sender,
189: address(this),
190: amount
191: );
192: }
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L198
```solidity
File: /src/Lender.sol
198: function removeFromPool(bytes32 poolId, uint256 amount) external {
199: if (pools[poolId].lender != msg.sender) revert Unauthorized(); //@audit first call
200: if (amount == 0) revert PoolConfig();
201: _updatePoolBalance(poolId, pools[poolId].poolBalance - amount); //@audit second call
202: // transfer the loan tokens from the contract to the lender
203: IERC20(pools[poolId].loanToken).transfer(msg.sender, amount); //@audit third call
204: }
```
```diff
File: /src/Lender.sol
198: function removeFromPool(bytes32 poolId, uint256 amount) external {
+ Pool calldata pool = pools[poolId];
-199: if (pools[poolId].lender != msg.sender) revert Unauthorized();
+199: if (pool.lender != msg.sender) revert Unauthorized();
200: if (amount == 0) revert PoolConfig();
-201: _updatePoolBalance(poolId, pools[poolId].poolBalance - amount);
+201: _updatePoolBalance(poolId, pool.poolBalance - amount);
202: // transfer the loan tokens from the contract to the lender
-203: IERC20(pools[poolId].loanToken).transfer(msg.sender, amount);
+203: IERC20(pool.loanToken).transfer(msg.sender, amount);
204: }
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L210
```solidity
File: /src/Lender.sol
210: function updateMaxLoanRatio(bytes32 poolId, uint256 maxLoanRatio) external {
211: if (pools[poolId].lender != msg.sender) revert Unauthorized();
212: if (maxLoanRatio == 0) revert PoolConfig();
213: pools[poolId].maxLoanRatio = maxLoanRatio;
214: emit PoolMaxLoanRatioUpdated(poolId, maxLoanRatio);
215: }
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L221C3-L226C6
```solidity
File: /src/Lender.sol
221: function updateInterestRate(bytes32 poolId, uint256 interestRate) external {
222: if (pools[poolId].lender != msg.sender) revert Unauthorized();
223: if (interestRate > MAX_INTEREST_RATE) revert PoolConfig();
224: pools[poolId].interestRate = interestRate;
225: emit PoolInterestRateUpdated(poolId, interestRate);
226: }
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L416C14-L420C44
```solidity
416: loans[loanId].lender = pool.lender;
417: loans[loanId].interestRate = pool.interestRate;
418: loans[loanId].startTimestamp = block.timestamp;
419: loans[loanId].auctionStartTimestamp = type(uint256).max;
420: loans[loanId].debt = totalDebt;
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L518
```solidity
518: loans[loanId].lender = msg.sender;
519: loans[loanId].interestRate = pools[poolId].interestRate;
520: loans[loanId].startTimestamp = block.timestamp;
521: loans[loanId].auctionStartTimestamp = type(uint256).max;
522: loans[loanId].debt = totalDebt;
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L686C13-L696C48
```solidity
File: /src/Lender.sol
686: loans[loanId].collateral = collateral;
687: // update loan interest rate
688: loans[loanId].interestRate = pool.interestRate;
689: // update loan start timestamp
690: loans[loanId].startTimestamp = block.timestamp;
691: // update loan auction start timestamp
692: loans[loanId].auctionStartTimestamp = type(uint256).max;
693: // update loan auction length
694: loans[loanId].auctionLength = pool.auctionLength;
695: // update loan lender
696: loans[loanId].lender = pool.lender;
```
When you declare a variable inside a loop, Solidity creates a new instance of the variable for each iteration of the loop. This can lead to unnecessary gas costs, especially if the loop is executed frequently or iterates over a large number of elements.
By declaring the variable outside the loop, you can avoid the creation of multiple instances of the variable and reduce the gas cost of your contract. Here's an example:
gas save 8*100= 800
```solidity
File: /src/Lender.sol
233: for (uint256 i = 0; i < borrows.length; i++) {
234: bytes32 poolId = borrows[i].poolId;
235: uint256 debt = borrows[i].debt;
236: uint256 collateral = borrows[i].collateral;
```
```solidity
File: /src/Lender.sol
294: uint256 loanId = loanIds[i];
```
```solidity
File: /src/Lender.sol
360: uint256 loanId = loanIds[i];
361: bytes32 poolId = poolIds[i];
```
```solidity
File: /src/Lender.sol
439: uint256 loanId = loanIds[i];
```
```solidity
File: /src/Lender.sol
550: uint256 loanId = loanIds[i];
```
Here, the values emitted shouldn’t be read from storage. The existing memory values should be used instead:
Gas save 100 per instances
total gas 400
```solidity
File: /src/Lender.sol
422: emit Borrowed(
423: loan.borrower,
424: pool.lender,
425: loanId,
426: loans[loanId].debt,
427: loans[loanId].collateral,
428: pool.interestRate,
429: block.timestamp
430: );
```
```diff
File: /src/Lender.sol
422: emit Borrowed(
423: loan.borrower,
424: pool.lender,
425: loanId,
-426: loans[loanId].debt,
+426: totalDebt,
-427: loans[loanId].collateral,
+427: loan.collateral,
428: pool.interestRate,
429: block.timestamp
430: );
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L524C9-L532C11
```solidity
File: /src/Lender.sol
524: emit Borrowed(
525: loan.borrower,
526: msg.sender,
527: loanId,
528: loans[loanId].debt,
529: loans[loanId].collateral,
530: pools[poolId].interestRate,
531: block.timestamp
532: );
```
```diff
File: /src/Lender.sol
524: emit Borrowed(
525: loan.borrower,
526: msg.sender,
527: loanId,
-528: loans[loanId].debt,
+528: totalDebt,
-529: loans[loanId].collateral,
+529: loan.collateral,
-530: pools[poolId].interestRate,
+530: pool.interestRate,
531: block.timestamp
532: );
```
Caching of a state variable replaces each Gwarmaccess (100 gas) with a cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.
Gas save (200)
```solidity
File: src/Fees.sol
26: function sellProfits(address _profits) public {
27: require(_profits != WETH, "not allowed");
28: uint256 amount = IERC20(_profits).balanceOf(address(this));
29:
30: ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
31: .ExactInputSingleParams({
32: tokenIn: _profits,
33: tokenOut: WETH,
34: fee: 3000,
35: recipient: address(this),
36: deadline: block.timestamp,
37: amountIn: amount,
38: amountOutMinimum: 0,
39: sqrtPriceLimitX96: 0
40: });
41:
42: amount = swapRouter.exactInputSingle(params);
43: IERC20(WETH).transfer(staking, IERC20(WETH).balanceOf(address(this)));
44: }
```
```diff
File: src/Fees.sol
26: function sellProfits(address _profits) public {
-27: require(_profits != WETH, "not allowed");
+27: require(_profits != _WETH, "not allowed");
28: uint256 amount = IERC20(_profits).balanceOf(address(this));
29:
30: ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
31: .ExactInputSingleParams({
32: tokenIn: _profits,
-33: tokenOut: WETH,
+33: tokenOut: _WETH,
34: fee: 3000,
35: recipient: address(this),
36: deadline: block.timestamp,
37: amountIn: amount,
38: amountOutMinimum: 0,
39: sqrtPriceLimitX96: 0
40: });
41:
42: amount = swapRouter.exactInputSingle(params);
-43: IERC20(WETH).transfer(staking, IERC20(WETH).balanceOf(address(this)));
+43: IERC20(_WETH).transfer(staking, IERC20(_WETH).balanceOf(address(this)));
44: }
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L656C13-L656C76
```solidity
File: /src/Lender.sol
//@auidt feeReceiver see line 651
656: IERC20(loan.loanToken).transfer(feeReceiver, protocolInterest);
```
mappings that share an ID can be combined into a single mapping of ID / structThis can avoid a Gsset (20000 Gas) per mapping combined. Reads and writes will also be cheaper when a function requires both values as they both can fit in the same storage slot.��Finally, if both fields are accessed in the same function, this can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 Gas) and that calculation's associated stack operations.
Gas save 21538
```solidity
File: [/src/Staking.sol](https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Staking.sol#L21)
21: /// @notice mapping of user balances
22: mapping(address => uint256) public balances;
23: /// @notice mapping of user claimable rewards
24: mapping(address => uint256) public claimable;
```
By using a uint32 rather than a larger type for variables that track timestamps, one can save gas by using fewer storage slots per struct, at the expense of the protocol breaking after the year 2106 (when uint32 wraps). If this is an acceptable tradeoff, each slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct. Subsequent reads as well as writes have smaller gas savings
gas save 2000
```solidity
File: src/utils/Structs.sol
34: struct Loan {
/// @notice address of the lender
36: address lender;
/// @notice address of the borrower
38: address borrower;
/// @notice address of the loan token
40: address loanToken;
/// @notice address of the collateral token
42: address collateralToken;
/// @notice the amount borrowed
44: uint256 debt;
/// @notice the amount of collateral locked in the loan
46: uint256 collateral;
/// @notice the interest rate of the loan per second (in debt tokens)
48: uint256 interestRate;
/// @notice the timestamp of the loan start
50: uint256 startTimestamp;
/// @notice the timestamp of a refinance auction start
52: uint256 auctionStartTimestamp;
/// @notice the refinance auction length
54: uint256 auctionLength;
55: }
```
```diff
File: src/utils/Structs.sol
34: struct Loan {
/// @notice address of the lender
36: address lender;
/// @notice address of the borrower
38: address borrower;
/// @notice address of the loan token
40: address loanToken;
/// @notice address of the collateral token
42: address collateralToken;
/// @notice the amount borrowed
44: uint256 debt;
/// @notice the amount of collateral locked in the loan
46: uint256 collateral;
/// @notice the interest rate of the loan per second (in debt tokens)
48: uint256 interestRate;
/// @notice the timestamp of the loan start
-50: uint256 startTimestamp;
- /// @notice the timestamp of a refinance auction start
-52: uint256 auctionStartTimestamp;
+50: uint32 startTimestamp;
+ /// @notice the timestamp of a refinance auction start
+52: uint32 auctionStartTimestamp;
/// @notice the refinance auction length
54: uint256 auctionLength;
55: }
```
++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow, as is the case when used in for- and while-loopsThe unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas per loop
Gas save 360
payablePayable functions cost less gas to execute, since the compiler does not have to add extra checks to ensure that a payment wasn't provided. A constructor can safely be marked as payable, since only the deployer would be able to pass funds, and the project itself would not pass any funds.
Gas save 84
```solidity
31: constructor(address _token, address _weth) Ownable(msg.sender) {
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L73
```solidity
73: constructor() Ownable(msg.sender) {
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Fees.sol#L19
```solidity
19: constructor(address _weth, address _staking) {
```
- https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Beedle.sol#L11C5-L11C85
```solidity
11: constructor() ERC20("Beedle", "BDL") ERC20Permit("Beedle") Ownable(msg.sender) {
```
While the event is emitted in blockchain, an event also consist of block.timestamp Therefore a gas can be saved by removing the explicitly block.timestamp from event.
```solidity
File: /src/Lender.sol
277: emit Borrowed(
278: msg.sender,
279: pool.lender,
280: loans.length - 1,
281: debt,
282: collateral,
283: pool.interestRate,
284: block.timestamp
285: );
....
422: emit Borrowed(
423: loan.borrower,
424: pool.lender,
425: loanId,
426: loans[loanId].debt,
427: loans[loanId].collateral,
428: pool.interestRate,
429: block.timestamp
430: );
....
449: emit AuctionStart(
450: loan.borrower,
451: loan.lender,
452: loanId,
453: loan.debt,
454: loan.collateral,
455: block.timestamp,
456: loan.auctionLength
457: );
....
524: emit Borrowed(
525: loan.borrower,
526: msg.sender,
527: loanId,
528: loans[loanId].debt,
529: loans[loanId].collateral,
530: pools[poolId].interestRate,
531: block.timestamp
532: );
....
699: emit Borrowed(
700: msg.sender,
701: pool.lender,
702: loanId,
703: debt,
704: collateral,
705: pool.interestRate,
706: block.timestamp
707: );
```
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.