Platform: Code4rena
Start Date: 07/04/2022
Pot Size: $100,000 USDC
Total HM: 20
Participants: 62
Period: 7 days
Judge: LSDan
Total Solo HM: 11
Id: 107
League: ETH
Rank: 54/62
Findings: 1
Award: $82.45
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Dravee
Also found by: 0v3rf10w, 0x1f8b, 0xDjango, 0xNazgul, 0xkatana, Cityscape, Cr4ckM3, FSchmoede, Foundation, Funen, Hawkeye, IllIllI, JMukesh, Meta0xNull, PPrieditis, Picodes, TerrierLover, Tomio, WatchPug, berndartmueller, catchup, delfin454000, dirk_y, ellahi, hickuphh3, ilan, kebabsec, kenta, nahnah, rayn, rfa, robee, rokinot, saian, securerodd, slywaters, sorrynotsorry
82.4497 USDC - $82.45
Strings are stored in slots of 32 bytes, and hence the length of the revert string should be at max 32 bytes to fit inside 1 slot. If the string is just 33 bytes it will occupy 2 slots (64 bytes). Keeping the string size at 32 bytes or below will save gas on both deployment and when the revert condition is met.
Examples revert strings longer than 32 bytes:
JPEG.sol line 23: "JPEG: must have minter role to mint" StableCoin.sol line 41: "StableCoin: must have minter role to mint" StableCoin.sol line 55: "StableCoin: must have pauser role to pause" StableCoin.sol line 69: "StableCoin: must have pauser role to unpause" NFTVault.sol line 394: "credit_rate_exceeds_or_equals_liquidation_rate"
The state variable totalDebtAmount
is read 2 times from memory on line 585 and 590. By caching the variable into memory one of the expensive SLOAD operations can be saved.
The code
function _calculateAdditionalInterest() internal view returns (uint256) { // Number of seconds since {accrue} was called uint256 elapsedTime = block.timestamp - totalDebtAccruedAt; if (elapsedTime == 0) { return 0; } if (totalDebtAmount == 0) { // SLOAD1 return 0; } // Accrue interest uint256 interestPerYear = (totalDebtAmount * // SLOAD2 settings.debtInterestApr.numerator) / settings.debtInterestApr.denominator; uint256 interestPerSec = interestPerYear / 365 days; return elapsedTime * interestPerSec; }
Can be changed into
function _calculateAdditionalInterest() internal view returns (uint256) { // Number of seconds since {accrue} was called uint256 elapsedTime = block.timestamp - totalDebtAccruedAt; if (elapsedTime == 0) { return 0; } uint256 debtAmount = totalDebtAmount; // SLOAD if (debtAmount == 0) { // MLOAD1 return 0; } // Accrue interest uint256 interestPerYear = (debtAmount * // MLOAD2 settings.debtInterestApr.numerator) / settings.debtInterestApr.denominator; uint256 interestPerSec = interestPerYear / 365 days; return elapsedTime * interestPerSec; }
This will instead use 1 SLOAD and 2 MLOAD operations, which is by far cheaper than 2 SLOAD operations. However, this will be a bit more expensive if the totalDebtAmount == 0
condition is met, since that will cost 1 SLOAD and 1 MLOAD compared to only 1 SLOAD. But I think the gas savings when the zero condition is not met weights up for this.
The state variable position.borrowType
is read 3 times; 2 times in require statements (line 700 and 719) and in one if-statement (line 728). The variable should be cached to save 2 SLOAD operations.
The code
require( position.borrowType == BorrowType.NOT_CONFIRMED || // SLOAD1 (position.borrowType == BorrowType.USE_INSURANCE && _useInsurance) || (position.borrowType == BorrowType.NON_INSURANCE && !_useInsurance), "invalid_insurance_mode" ); uint256 creditLimit = _getCreditLimit(_nftIndex); uint256 debtAmount = _getDebtAmount(_nftIndex); require(debtAmount + _amount <= creditLimit, "insufficient_credit"); //calculate the borrow fee uint256 organizationFee = (_amount * settings.organizationFeeRate.numerator) / settings.organizationFeeRate.denominator; uint256 feeAmount = organizationFee; //if the position is insured, calculate the insurance fee if (position.borrowType == BorrowType.USE_INSURANCE || _useInsurance) { // SLOAD2 feeAmount += (_amount * settings.insurancePurchaseRate.numerator) / settings.insurancePurchaseRate.denominator; } totalFeeCollected += feeAmount; //subtract the fee from the amount borrowed stablecoin.mint(msg.sender, _amount - feeAmount); if (position.borrowType == BorrowType.NOT_CONFIRMED) { // SLOAD3 position.borrowType = _useInsurance ? BorrowType.USE_INSURANCE : BorrowType.NON_INSURANCE; }
Can be changed into
BorrowType borrowType = position.borrowType; // SLOAD1 require( borrowType == BorrowType.NOT_CONFIRMED || // MLOAD1 (borrowType == BorrowType.USE_INSURANCE && _useInsurance) || (borrowType == BorrowType.NON_INSURANCE && !_useInsurance), "invalid_insurance_mode" ); uint256 creditLimit = _getCreditLimit(_nftIndex); uint256 debtAmount = _getDebtAmount(_nftIndex); require(debtAmount + _amount <= creditLimit, "insufficient_credit"); //calculate the borrow fee uint256 organizationFee = (_amount * settings.organizationFeeRate.numerator) / settings.organizationFeeRate.denominator; uint256 feeAmount = organizationFee; //if the position is insured, calculate the insurance fee if (borrowType == BorrowType.USE_INSURANCE || _useInsurance) { // MLOAD2 feeAmount += (_amount * settings.insurancePurchaseRate.numerator) / settings.insurancePurchaseRate.denominator; } totalFeeCollected += feeAmount; //subtract the fee from the amount borrowed stablecoin.mint(msg.sender, _amount - feeAmount); if (borrowType == BorrowType.NOT_CONFIRMED) { // MLOAD3 position.borrowType = _useInsurance ? BorrowType.USE_INSURANCE : BorrowType.NON_INSURANCE; }
This will instead use 1 SLOAD and 3 MLOAD operations, which is by far cheaper than 3 SLOAD operations.