Platform: Code4rena
Start Date: 12/08/2022
Pot Size: $50,000 USDC
Total HM: 15
Participants: 120
Period: 5 days
Judge: Justin Goro
Total Solo HM: 6
Id: 153
League: ETH
Rank: 21/120
Findings: 2
Award: $247.69
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0x1f8b
Also found by: 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xmatt, 0xsolstars, Aymen0909, Bnke0x0, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, EthLedger, Funen, IllIllI, JC, Junnon, Lambda, LeoS, MiloTruck, Noah3o6, PaludoX0, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, SaharAP, Sm4rty, SooYa, The_GUILD, TomJ, Waze, Yiko, _Adam, __141345__, a12jmx, ak1, asutorufos, auditor0517, ayeslick, ballx, beelzebufo, berndartmueller, bin2chen, brgltd, c3phas, cRat1st0s, cccz, cryptonue, cryptphi, d3e4, delfin454000, dipp, djxploit, durianSausage, dy, erictee, fatherOfBlocks, gogo, gzeon, hyh, ignacio, kyteg, ladboy233, medikko, mics, minhquanym, oyc_109, pfapostol, rbserver, reassor, ret2basic, robee, sach1r0, simon135, sryysryy, tabish, yac, yash90, zzzitron
226.1154 USDC - $226.12
Low Risk Issues
Issue | Instances | |
---|---|---|
1 | Missing checks for address(0x0) when assigning values to address state variables | 9 |
Non-critical Issues
Issue | Instances | |
---|---|---|
1 | approve return values not checked | 2 |
2 | Double check for overflow | 3 |
3 | constants should be defined rather than using magic numbers | 7 |
4 | public functions not called by the contract should be declared external instead | 5 |
5 | Unused Parameter | 1 |
6 | Unused Named Return | 1 |
7 | Function parameter missing from NatSpec | 1 |
8 | Redundant type cast uint256 | 1 |
9 | Two contracts the similar names | 1 |
10 | Non-library/interface files should use fixed compiler versions, not floating ones | 7 |
Total: 38 instances over 11 issues
Low Risk:
Missing checks for address(0x0) when assigning values to address state variables (9 instances)
Zero-address checks are a best practice for input validation of critical address parameters. Accidental use of zero-addresses may result in exceptions, burn fees/tokens.
104 CIRCUIT_BREAKER_ADDRESS = _circuitBreaker; 105 COMPTROLLER_ADDRESS = _comptroller; 106 TIME_LOCK_ADDRESS = _timelock; 107 FRAXLEND_WHITELIST_ADDRESS = _fraxlendWhitelist;
172 CIRCUIT_BREAKER_ADDRESS = _circuitBreaker; 173 COMPTROLLER_ADDRESS = _comptrollerAddress; 174 TIME_LOCK_ADDRESS = _timeLockAddress; 175 FRAXLEND_WHITELIST_ADDRESS = _fraxlendWhitelistAddress;
206 TIME_LOCK_ADDRESS = _newAddress;
Non-Critical:
approve
return values not checked (2 instances)
I chose not critical as the transaction will fail if the approval is invalid. But it can be considered as low, since the user will spend significantly more when calling an external function than if it were reverted immediately.
1103 _assetContract.approve(_swapperAddress, _borrowAmount); ... 1184 _collateralContract.approve(_swapperAddress, _collateralToSwap);
fix:
if (!_assetContract.approve(_swapperAddress, _borrowAmount)) revert NotApproved();
OR: use safeApprove
(just change function, because _assetContract
and _collateralContract
is already using SafeERC20
)
Double check for overflow (3 instances)
Arithmetic operations revert on underflow and overflow. You can use unchecked { ... } to use the previous wrapping behaviour.
471 if ( 472 _interestEarned + _totalBorrow.amount <= type(uint128).max && 473 _interestEarned + _totalAsset.amount <= type(uint128).max 474 ) { 475 _totalBorrow.amount += uint128(_interestEarned); 476 _totalAsset.amount += uint128(_interestEarned); ... 542 if (_exchangeRate > type(uint224).max) revert PriceTooLarge(); 543 _exchangeRateInfo.exchangeRate = uint224(_exchangeRate); ... 633 if (allowed != type(uint256).max) _approve(_owner, msg.sender, allowed - _shares);
fix:
Both fixes save gas.
constants
should be defined rather than using magic numbers (7 instances)
468 _interestEarned = (_deltaTime * _totalBorrow.amount * _currentRateInfo.ratePerSec) / 1e18; ... 522 uint256 _price = uint256(1e36);
69 uint256 _deltaUtilization = ((MIN_UTIL - _utilization) * 1e18) / MIN_UTIL; ... 76 uint256 _deltaUtilization = ((_utilization - MAX_UTIL) * 1e18) / (UTIL_PREC - MAX_UTIL);
171 bytes memory _firstHalf = BytesLib.slice(_creationCode, 0, 13000); ... 173 if (_creationCode.length > 13000) { 174 bytes memory _secondHalf = BytesLib.slice(_creationCode, 13000, _creationCode.length - 13000);
public
functions not called by the contract should be declared external
instead (5 instances)
Contracts are allowed to override their parents' functions and change the visibility from external
to public
.
74 function name() public view override(ERC20, IERC20Metadata) returns (string memory) { ... 78 function symbol() public view override(ERC20, IERC20Metadata) returns (string memory) { ... 84 function decimals() public pure override(ERC20, IERC20Metadata) returns (uint8) { ... 89 function totalSupply() public view override(ERC20, IERC20) returns (uint256) { ... 100 function totalAssets() public view virtual returns (uint256) {
Unused Parameter (1 instances)
Removing unused named return variables can reduce gas usage and improve code clarity.
63 function getNewRate(bytes calldata _data, bytes calldata _initData)
Unused Named Return (1 instances)
return is redundant when using named returns
519 return _exchangeRate = _exchangeRateInfo.exchangeRate;
Function parameter missing from NatSpec (1 instances)
Makes it difficult for the user to understand the parameter
153 bytes memory _immutables,
Redundant type cast uint256 (1 instances)
522 uint256 _price = uint256(1e36);
Two contracts the similar names (1 instances)
Detected with: slither - https://github.com/crytic/slither/wiki/Detector-Documentation#name-reused
If a codebase has two contracts the similar names, the compilation artifacts will not contain one of the contracts with the duplicate name.
SafeERC20 is re-used:
Non-library/interface files should use fixed compiler versions, not floating ones (7 instances)
2022-08-frax/src/contracts/FraxlendPair.sol
2022-08-frax/src/contracts/FraxlendPairConstants.sol
2022-08-frax/src/contracts/FraxlendPairCore.sol
2022-08-frax/src/contracts/FraxlendPairDeployer.sol
2022-08-frax/src/contracts/FraxlendWhitelist.sol
2022-08-frax/src/contracts/LinearInterestRate.sol
2022-08-frax/src/contracts/VariableInterestRate.sol
pragma solidity ^0.8.15;
#0 - gititGoro
2022-10-07T01:21:29Z
ERC20 compliance out of scope. For name collisions, the dev used aliasing. Otherwise, very good report!
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xA5DF, 0xDjango, 0xNazgul, 0xSmartContract, 0xackermann, 0xbepresent, 0xc0ffEE, 0xkatana, 2997ms, Amithuddar, Aymen0909, Bnke0x0, Chinmay, Chom, CodingNameKiki, Deivitto, Diraco, Dravee, ElKu, EthLedger, Fitraldys, Funen, IgnacioB, JC, Junnon, Lambda, LeoS, Metatron, MiloTruck, Noah3o6, NoamYakov, PaludoX0, Randyyy, ReyAdmirado, Rohan16, Rolezn, Ruhum, SaharAP, Sm4rty, SooYa, TomJ, Tomio, Waze, Yiko, _Adam, __141345__, a12jmx, ajtra, ak1, asutorufos, ballx, brgltd, c3phas, cRat1st0s, carlitox477, chrisdior4, d3e4, delfin454000, dharma09, djxploit, durianSausage, erictee, fatherOfBlocks, find_a_bug, flyx, francoHacker, gerdusx, gogo, gzeon, hakerbaya, ignacio, jag, kyteg, ladboy233, ltyu, m_Rassska, medikko, mics, mrpathfindr, newfork01, nxrblsrpr, oyc_109, pfapostol, rbserver, reassor, ret2basic, robee, sach1r0, saian, simon135, sryysryy, zeesaw
21.565 USDC - $21.57
Gas savings are estimated using gas report of existing tests source .env && forge test --fork-url $MAINNET_URL --fork-block-number $DEFAULT_FORK_BLOCK --gas-report
(the sum of all deployment costs and the sum of the cost of calling all methods) and may vary depending on the implementation of the fix. I keep my version of the fix for each finding and can provide them if you need. The accuracy of the estimate depends on the test coverage. I'm not "foundry-friendly", and unfortunately I didn't have enough time to learn how to write additional tests.
Gas Optimizations
Issue | Instances | Estimated gas(deployments) | Estimated gas(method call) | |
---|---|---|---|---|
1 | Error handling | optional | ||
1.1 | Use Custom Errors instead of Revert/Require Strings to save Gas | 9 | 155 360 | - |
1.2 | Require()/revert() strings longer than 32 bytes cost extra gas | 8 | 70 073 | - |
1.3 | Splitting require() statements that use && saves gas | 3 | - | - |
2 | Variable can be a constant | 4 | 108 477 | ≈66 179 |
3 | Increment optimization. | optional | ||
3.1 | Prefix increments are cheaper than postfix increments, especially when it's used in for-loops | 9 | 3 613 | ≈6 305 |
3.2 | <x> = <x> + 1 even more efficient than pre increment | 11 | 6 607 | ≈14 176 |
3.3 | Expression can be unchecked when overflow is not possible | 11 | 45 459 | ≈59 563 |
4 | State variables only set in the constructor should be declared immutable | 3 | 8 352 | ≈1 841 |
5 | x = x + y is more efficient than x += y | 22 | 8 009 | ≈22 006 |
6 | Using bools for storage incurs overhead | 9 | 6 855 | ≈26 630 |
7 | It costs more gas to initialize non-constant/non-immutable variables to zero than to let the default of zero be applied | 11 | 807 | ≈1 101 |
Total: 100 instances over 11 issues
Error handling
Use Custom Errors instead of Revert/Require Strings to save Gas (9 instances)
Deployment Gas Saved: 155360
Solidity 0.8.4 introduced custom errors. They are more gas efficient than revert strings, when it comes to deploy cost as well as runtime cost when the revert condition is met. Use custom errors instead of revert strings for gas savings.
Custom errors save ~50 gas each time they're hitby avoiding having to allocate and store the revert string. Additional gas is saved due to the lack of defining string. https://blog.soliditylang.org/2021/04/21/custom-errors/#errors-in-depth
57 require( 58 _minInterest < MAX_INT && _minInterest <= _vertexInterest && _minInterest >= MIN_INT, 59 "LinearInterestRate: _minInterest < MAX_INT && _minInterest <= _vertexInterest && _minInterest >= MIN_INT" 60 ); 61 require( 62 _maxInterest <= MAX_INT && _vertexInterest <= _maxInterest && _maxInterest > MIN_INT, 63 "LinearInterestRate: _maxInterest <= MAX_INT && _vertexInterest <= _maxInterest && _maxInterest > MIN_INT" 64 ); 65 require( 66 _vertexUtilization < MAX_VERTEX_UTIL && _vertexUtilization > 0, 67 "LinearInterestRate: _vertexUtilization < MAX_VERTEX_UTIL && _vertexUtilization > 0" 68 );
205 require(deployedPairsBySalt[salt] == address(0), "FraxlendPairDeployer: Pair already deployed"); ... 228 require(_pairAddress != address(0), "FraxlendPairDeployer: create2 failed"); ... 253 require(deployedPairsByName[_name] == address(0), "FraxlendPairDeployer: Pair name must be unique"); ... 365 require(_maxLTV <= GLOBAL_MAX_LTV, "FraxlendPairDeployer: _maxLTV is too large"); 366 require( 367 IFraxlendWhitelist(FRAXLEND_WHITELIST_ADDRESS).fraxlendDeployerWhitelist(msg.sender), 368 "FraxlendPairDeployer: Only whitelisted addresses" 369 ); ... 399 require(msg.sender == CIRCUIT_BREAKER_ADDRESS, "Circuit Breaker only");
Require()/revert() strings longer than 32 bytes cost extra gas (8 instances)
If you opt not to use custom errors keeping require strings <= 32 bytes in length will save gas.
Deployment Gas Saved: 70073
57 require( 58 _minInterest < MAX_INT && _minInterest <= _vertexInterest && _minInterest >= MIN_INT, 59 "LinearInterestRate: _minInterest < MAX_INT && _minInterest <= _vertexInterest && _minInterest >= MIN_INT" 60 ); 61 require( 62 _maxInterest <= MAX_INT && _vertexInterest <= _maxInterest && _maxInterest > MIN_INT, 63 "LinearInterestRate: _maxInterest <= MAX_INT && _vertexInterest <= _maxInterest && _maxInterest > MIN_INT" 64 ); 65 require( 66 _vertexUtilization < MAX_VERTEX_UTIL && _vertexUtilization > 0, 67 "LinearInterestRate: _vertexUtilization < MAX_VERTEX_UTIL && _vertexUtilization > 0" 68 );
205 require(deployedPairsBySalt[salt] == address(0), "FraxlendPairDeployer: Pair already deployed"); ... 228 require(_pairAddress != address(0), "FraxlendPairDeployer: create2 failed"); ... 253 require(deployedPairsByName[_name] == address(0), "FraxlendPairDeployer: Pair name must be unique"); ... 365 require(_maxLTV <= GLOBAL_MAX_LTV, "FraxlendPairDeployer: _maxLTV is too large"); 366 require( 367 IFraxlendWhitelist(FRAXLEND_WHITELIST_ADDRESS).fraxlendDeployerWhitelist(msg.sender), 368 "FraxlendPairDeployer: Only whitelisted addresses" 369 );
Splitting require() statements that use && saves gas (3 instances)
Optional, if you do not want to use model if () revert CustomError();
Costs extra gas to deploy, but saves gas in the long run due to cheaper method calls
57 require( 58 _minInterest < MAX_INT && _minInterest <= _vertexInterest && _minInterest >= MIN_INT, 59 "LinearInterestRate: _minInterest < MAX_INT && _minInterest <= _vertexInterest && _minInterest >= MIN_INT" 60 ); 61 require( 62 _maxInterest <= MAX_INT && _vertexInterest <= _maxInterest && _maxInterest > MIN_INT, 63 "LinearInterestRate: _maxInterest <= MAX_INT && _vertexInterest <= _maxInterest && _maxInterest > MIN_INT" 64 ); 65 require( 66 _vertexUtilization < MAX_VERTEX_UTIL && _vertexUtilization > 0, 67 "LinearInterestRate: _vertexUtilization < MAX_VERTEX_UTIL && _vertexUtilization > 0" 68 );
Variable can be a constant (4 instances)
Deployment Gas Saved: 108477 Method Call Gas Saved: ≈66179
48 // Constants 49 uint256 public DEFAULT_MAX_LTV = 75000; // 75% with 1e5 precision 50 uint256 public GLOBAL_MAX_LTV = 1e8; // 1000x (100,000%) with 1e5 precision, protects from rounding errors in LTV calc 51 uint256 public DEFAULT_LIQ_FEE = 10000; // 10% with 1e5 precision
fix:
49 uint256 private constant DEFAULT_MAX_LTV = 75000; // 75% with 1e5 precision 50 uint256 private constant GLOBAL_MAX_LTV = 1e8; // 1000x (100,000%) with 1e5 precision, protects from rounding errors in LTV calc 51 uint256 private constant DEFAULT_LIQ_FEE = 10000; // 10% with 1e5 precision
51 string public version = "1.0.0";
fix:
51 string private constant version = "1.0.0";
Increment optimization.
Prefix increments are cheaper than postfix increments, especially when it's used in for-loops (9 instances).
Deployment Gas Saved: 3613 Method Call Gas Saved: ≈6305
24 i++; ... 27 for (i = 0; i < 32 && data[i] != 0; i++) {
130 i++; ... 158 i++;
51 for (uint256 i = 0; i < _addresses.length; i++) { ... 66 for (uint256 i = 0; i < _addresses.length; i++) { ... 81 for (uint256 i = 0; i < _addresses.length; i++) {
289 for (uint256 i = 0; i < _lenders.length; i++) { ... 308 for (uint256 i = 0; i < _borrowers.length; i++) {
<x> = <x> + 1 even more efficient than pre increment.(11 instances) Gas saved:
Deployment Gas Saved: 6607 Method Call Gas Saved: ≈14176
24 i++; ... 27 for (i = 0; i < 32 && data[i] != 0; i++) {
130 i++; ... 158 i++;
51 for (uint256 i = 0; i < _addresses.length; i++) { ... 66 for (uint256 i = 0; i < _addresses.length; i++) { ... 81 for (uint256 i = 0; i < _addresses.length; i++) {
289 for (uint256 i = 0; i < _lenders.length; i++) { ... 308 for (uint256 i = 0; i < _borrowers.length; i++) {
265 for (uint256 i = 0; i < _approvedBorrowers.length; ++i) { ... 270 for (uint256 i = 0; i < _approvedLenders.length; ++i) {
Expression can be unchecked when overflow is not possible.(11 instances) Gas saved:
Deployment Gas Saved: 45459 Method Call Gas Saved: ≈59563
24 i++; ... 27 for (i = 0; i < 32 && data[i] != 0; i++) {
51 for (uint256 i = 0; i < _addresses.length; i++) { ... 66 for (uint256 i = 0; i < _addresses.length; i++) { ... 81 for (uint256 i = 0; i < _addresses.length; i++) {
289 for (uint256 i = 0; i < _lenders.length; i++) { ... 308 for (uint256 i = 0; i < _borrowers.length; i++) {
265 for (uint256 i = 0; i < _approvedBorrowers.length; ++i) { ... 270 for (uint256 i = 0; i < _approvedLenders.length; ++i) {
26 shares = shares + 1; // @note Theoretically, it cannot overflow, because the result of division in 24 ... 43 amount = amount + 1; // @note Theoretically, it cannot overflow, because the result of division in 41
State variables only set in the constructor should be declared immutable (3 instances)
Avoids a Gsset (20000 gas) in the constructor, and replaces each Gwarmacces (100 gas) with a PUSH32 (3 gas).
Deployment Gas Saved: 8352 Method Call Gas Saved: ≈1841
57 address public CIRCUIT_BREAKER_ADDRESS; 58 address public COMPTROLLER_ADDRESS; 59 address public TIME_LOCK_ADDRESS;
x = x + y is more efficient than x += y (22 instances)
Deployment Gas Saved: 8009 Method Call Gas Saved: ≈22006
475 _totalBorrow.amount += uint128(_interestEarned); 476 _totalAsset.amount += uint128(_interestEarned); ... 484 _totalAsset.shares += uint128(_feesShare); ... 566 _totalAsset.amount += _amount; 567 _totalAsset.shares += _shares; ... 643 _totalAsset.amount -= _amountToReturn; 644 _totalAsset.shares -= _shares; ... 718 _totalBorrow.amount += _borrowAmount; 719 _totalBorrow.shares += uint128(_sharesAdded); ... 724 userBorrowShares[msg.sender] += _sharesAdded; ... 772 userCollateralBalance[_borrower] += _collateralAmount; 773 totalCollateral += _collateralAmount; ... 813 userCollateralBalance[_borrower] -= _collateralAmount; ... 815 totalCollateral -= _collateralAmount; ... 863 _totalBorrow.amount -= _amountToRepay; 864 _totalBorrow.shares -= _shares; ... 867 userBorrowShares[_borrower] -= _shares; ... 1008 totalAsset.amount -= _amountToAdjust; ... 1011 _totalBorrow.amount -= _amountToAdjust; 1012 _totalBorrow.shares -= _sharesToAdjust;
252 _totalAsset.amount -= uint128(_amountToTransfer); 253 _totalAsset.shares -= _shares;
Using bools for storage incurs overhead (9 instances)
// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from 'false' to 'true', after having been 'true' in the past
Deployment Gas Saved: 6855 Method Call Gas Saved: ≈26630
32 mapping(address => bool) public oracleContractWhitelist; ... 35 mapping(address => bool) public rateContractWhitelist; ... 38 mapping(address => bool) public fraxlendDeployerWhitelist;
78 mapping(address => bool) public swappers;
133 bool public immutable borrowerWhitelistActive; // @note bool usage 134 mapping(address => bool) public approvedBorrowers; ... 136 bool public immutable lenderWhitelistActive; // @note bool usage 137 mapping(address => bool) public approvedLenders;
94 mapping(address => bool) public deployedPairCustomStatusByAddress;
It costs more gas to initialize non-constant/non-immutable variables to zero than to let the default of zero be applied (11 instances)
Deployment Gas Saved: 807 Method Call Gas Saved: ≈1101
22 uint8 i = 0;
51 for (uint256 i = 0; i < _addresses.length; i++) { ... 66 for (uint256 i = 0; i < _addresses.length; i++) { ... 81 for (uint256 i = 0; i < _addresses.length; i++) {
265 for (uint256 i = 0; i < _approvedBorrowers.length; ++i) { ... 270 for (uint256 i = 0; i < _approvedLenders.length; ++i) {
289 for (uint256 i = 0; i < _lenders.length; i++) { ... 308 for (uint256 i = 0; i < _borrowers.length; i++) {
127 for (i = 0; i < _lengthOfArray; ) { ... 152 for (i = 0; i < _lengthOfArray; ) { ... 402 for (uint256 i = 0; i < _lengthOfArray; ) {
#0 - gititGoro
2022-10-09T15:09:57Z
Very nicely presented!