Platform: Code4rena
Start Date: 28/11/2022
Pot Size: $192,500 USDC
Total HM: 33
Participants: 106
Period: 11 days
Judge: LSDan
Total Solo HM: 15
Id: 186
League: ETH
Rank: 56/106
Findings: 1
Award: $145.94
đ Selected for report: 0
đ Solo Findings: 0
đ Selected for report: IllIllI
Also found by: B2, Deivitto, Dravee, PaludoX0, RaymondFam, Rolezn, Sathish9098, _Adam, ahmedov, c3phas, chrisdior4, cyberinn, rjs, saneryee
145.9382 USDC - $145.94
The ParaSpace codebase is necessarily complex and does well to follow Solidity best practice. That being said, many areas of the ParaSpace protocol involve regular user-initiated transactions and therefore these hot paths should be considered for gas optimization.
Often gas optimization recommendations can be at odds with Solidity best practice, and developers must be sure to carefully balance deployment and user transaction costs against the risk of potentially introducing future issues if the assumptions behind gas optimized functions were to change.
While I would normally endeavour to provide estimates of gas improvements, there appears to be an issue with the ParaSpace-configured hardhat-gas-reporter
that I was not able to resolve in the limited time available.
ID | Finding | Instances |
---|---|---|
G-01 | Use custom, user-friendly errors (NOT identified in C4udit report) | 115 |
G-02 | Use unchecked blocks where overflow and underflow is not a valid concern | 1 |
G-03 | Use primitive types or a user-defined type  instead of a single member struct  | 2 |
G-04 | x = x + y  is more efficient than x += y; | 24 |
G-05 | Use transferFrom instead of safeTransferFrom where previously vetted | 12 |
G-06 | Use named returns  when possible | - |
Total: 154 instances over 6 issues
Deployment Gas saved: â (10,000s) Transaction Gas saved: â
Note that while the accompanying C4udit report did recommend the use of custom errors where strings were directly used, C4udit missed scenarios where indirect string constants were used i.e. in the Errors
library.
That being said, while the ParaSpace errors in the Errors
library are all stringified numeric codes and therefore have minor gas costs, I would nevertheless recommend using custom errors with more descriptive user-friendly errors, perhaps prefixed with codes to retain the benefits of error codes for front-end development.
Finally, I would recommend not mixing direct strings in revert()
vs. strings inside an Errors
library -- rather stick to a single convention i.e. custom errors.
There are 115 instances of this issue:
10: string public constant CALLER_NOT_POOL_ADMIN = "1"; // 'The caller of the function is not a pool admin' 11: string public constant CALLER_NOT_EMERGENCY_ADMIN = "2"; // 'The caller of the function is not an emergency admin' 12: string public constant CALLER_NOT_POOL_OR_EMERGENCY_ADMIN = "3"; // 'The caller of the function is not a pool or emergency admin' 13: string public constant CALLER_NOT_RISK_OR_POOL_ADMIN = "4"; // 'The caller of the function is not a risk or pool admin' 14: string public constant CALLER_NOT_ASSET_LISTING_OR_POOL_ADMIN = "5"; // 'The caller of the function is not an asset listing or pool admin' 15: string public constant CALLER_NOT_BRIDGE = "6"; // 'The caller of the function is not a bridge' ... 125: string public constant APE_STAKING_AMOUNT_NON_ZERO = "130"; //ape staking amount should be zero when supply bayc/mayc.
unchecked
blocks where overflow and underflow is not a valid concern (1 instance)Deployment Gas saved: â (1,000s) Transaction Gas saved: â
The complexity of the ParaSpace protocol involves a number of loops that are executed on every major transaction type in the various *Logic
contracts. In many of these loops, overflow and underflow is not a valid concern. The careful use of unchecked
in these core contracts can reduce transaction fees for customers.
283: assets.push(_asset); 284: assetFeederMap[_asset].index = uint8(assets.length - 1);
type
 instead of a single member struct
 (2 instances)Deployment Gas saved: â (10,000s) Transaction Gas saved: â (1,000s)
26: struct APEStakingParameter { 27: uint256 unstakeIncentive; 28: }
There is another instance in ParaReentrancyGuard
, but this structure is only ever referenced via SLOAD
and therefore changing this will only affect deployment gas:
39: struct RGStorage { 40: uint256 _status; 41: }
x = x + y
 is more efficient than x += y;
(24 instances)Deployment Gas saved: â (10,000s) Transaction Gas saved: â
149: token0Amount += positionData.tokensOwed0; 150: token1Amount += positionData.tokensOwed1; ... 211: sum += getTokenPrice(tokenIds[index]);
169: vars.payableDebtByERC20Assets += vars 170: .userBalanceInBaseCurrency 171: .percentDiv(vars.liquidationBonus); ... 176: vars.avgLtv += vars.userBalanceInBaseCurrency * vars.ltv; ... 12 other instances
397: price += item.startAmount;
252: reserve.accruedToTreasury += vars 253: .amountToMint 254: .rayDiv(reserveCache.nextLiquidityIndex) 255: .toUint128();
158: vars.currentVariableBorrowRate += 159: _variableRateSlope1 + 160: _variableRateSlope2.rayMul(excessBorrowUsageRatio); ... 162: vars.currentVariableBorrowRate += _variableRateSlope1 163: .rayMul(vars.borrowUsageRatio) 164: .rayDiv(OPTIMAL_USAGE_RATIO);
77: amountToWithdraw += _nfts[index].amount; ... 166: amountToWithdraw += _nftPairs[index].amount;
215: totalAmount += getTokenIdStakingAmount( 216: poolId, 217: _apeCoinStaking, 218: tokenId 219: ); ... 257: apeStakedAmount += bakcStakedAmount;
transferFrom
instead of safeTransferFrom
where previously vetted (12 instances)ParaSpace makes extensive use of safeTransferFrom()
on various hot paths.`
safeTransferFrom
builds on top of transferFrom
by performing additional safety checks e.g. if the token address is a contract , whether it implements onERC721Received
etc.
Since ParaSpace currently explicitly approves and vets all tokens and NFTs available in the protocol due to collateral pricing volatility concerns, it is perfectly reasonable to use transferFrom
and avoid these redundant checks in the hot paths of the protocol.
Some examples:
846: apeCoin.safeTransferFrom(msg.sender, address(this), _amount);
157: bakcContract.safeTransferFrom( 158: msg.sender, 159: xTokenAddress, 160: _nftPairs[index].bakcTokenId 161: );
110: nWPunk.safeTransferFrom(msg.sender, address(this), punkIndexes[i]);
returns
 when possibleDeployment Gas saved: â (1,000s) Transaction Gas saved: â
Named returns not only save deployment gas and improve code clarity, but reduce the number of lines of code, which will result in a cheaper audit next time around.
While much of the code does already use named returns e.g. getAuctionData(uint256 tokenId)@paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol
, there are still a number of instances of multi-line functions where this language feature is not being used, e.g.:
115: function getAssetPrice(address asset) 116: public 117: view 118: override 119: returns (uint256) // use returns (uint256 price) instead 120: { 121: if (asset == BASE_CURRENCY) { 122: return BASE_CURRENCY_UNIT; 123: } 124: 125: uint256 price = 0; 126: IEACAggregatorProxy source = IEACAggregatorProxy(assetsSources[asset]); 127: if (address(source) != address(0)) { 128: price = uint256(source.latestAnswer()); 129: } 130: if (price == 0 && address(_fallbackOracle) != address(0)) { 131: price = _fallbackOracle.getAssetPrice(asset); 132: } 133: 134: require(price != 0, Errors.ORACLE_PRICE_NOT_READY); 135: return price; // and this can be deleted 136: }
#0 - c4-judge
2023-01-25T17:01:05Z
dmvt marked the issue as grade-c
#1 - c4-judge
2023-01-25T17:01:12Z
dmvt marked the issue as grade-b