Platform: Code4rena
Start Date: 27/11/2023
Pot Size: $60,500 USDC
Total HM: 7
Participants: 72
Period: 7 days
Judge: Picodes
Total Solo HM: 2
Id: 309
League: ETH
Rank: 50/72
Findings: 1
Award: $19.82
🌟 Selected for report: 0
🚀 Solo Findings: 0
19.8173 USDC - $19.82
Issue | Instances | Gas Saved | |
---|---|---|---|
G-01 | ++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 | 2 | 120 |
G-02 | Use assembly to check for address(0) | 2 | 12 |
G-03 | Optimize External Calls with Assembly for Memory Efficiency | 11 | 2,420 |
G-04 | Simple checks for zero can be done using assembly to save gas | 13 | 78 |
G-05 | Use assembly to revert with an error message | 6 | 1,800 |
G-06 | Shorten the array rather than copying to a new one | 2 | - |
G-07 | Use assembly to calculate hashes to save gas | 1 | 120 |
G-08 | Use assembly to emit events | 8 | 304 |
G-09 | Use assembly to write address storage values | 1 | - |
G-10 | Avoid contract existence checks by using low level calls | 4 | 400 |
G-11 | The result of function calls should be cached rather than re-calling the function | 4 | 84 |
G-12 | State variables can be modified to fit in fewer storage slots | 2 | 40,000 |
G-13 | It costs more gas to initialize non-constant/non-immutable state variables to zero than to let the default of zero be applied | 7 | - |
G-14 | unchecked {} can be used on the division of two uint s in order to save gas | 4 | - |
G-15 | Optimize Deployment Size by Fine-tuning IPFS Hash | 1 | 220 |
G-16 | Trade-offs Between Modifiers and Internal Functions | 3 | - |
G-17 | Optimize Gas by Using Only Named Returns | 46 | 2,024 |
G-18 | Unused named return variables without optimizer waste gas | 5 | 18 |
G-19 | Pre-increments and pre-decrements are cheaper than post-increments and post-decrements | 1 | - |
G-20 | State variables which are not modified within functions should be set as constant or immutable for values set at deployment | 5 | 100,000 |
G-21 | private functions used once can be inlined | 1 | 20 |
G-22 | Use upcoming OpenZeppelin contracts | 1 | - |
Total: 528 instances over 51 issues with an estimate of 343,433 gas saved.
++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
Instances (2):
File: contracts/tokens/ERC1155Minimal.sol 187: for (uint256 i = 0; i < owners.length; ++i) {
File: contracts/types/TokenId.sol 468: for (uint256 i = 0; i < 4; ++i) {
assembly
to check for address(0)
Saves 6 gas per instance
Instances (2):
File: contracts/SemiFungiblePositionManager.sol 356: if (address(univ3pool) == address(0)) revert Errors.UniswapPoolNotInitialized(); 370: while (address(s_poolContext[poolId].pool) != address(0)) {
Using interfaces to make external contract calls in Solidity is convenient but can be inefficient in terms of memory utilization. Each such call involves creating a new memory location to store the data being passed, thus incurring memory expansion costs.
Inline assembly allows for optimized memory usage by re-using already allocated memory spaces or using the scratch space for smaller datasets. This can result in notable gas savings, especially for contracts that make frequent external calls.
Additionally, using inline assembly enables important safety checks like verifying if the target address has code deployed to it using extcodesize(addr)
before making the call, mitigating risks associated with contract interactions.
Instances (11):
<details> <summary>see instances</summary>File: contracts/SemiFungiblePositionManager.sol 354: 355: // reverts if the Uni v3 pool has not been initialized 773: // this is simply a convenience feature, and should be treated as such 774: if ((itm0 != 0) && (itm1 != 0)) { 840: /// @dev Loops over each leg in the tokenId and calls _createLegInAMM for each, which does the mint/burn in the AMM. 841: /// @param univ3pool the Uniswap pool. 842: /// @param tokenId the option position 843: /// @param positionSize the size of the option position 844: /// @param isBurn is true if the position is burnt 1158: // from msg.sender to the uniswap pool, stored as negative value to represent amount debited 1158: // from msg.sender to the uniswap pool, stored as negative value to represent amount debited 1165: /// @dev note that "moved" means: burn position in Uniswap and send tokens to msg.sender. 1166: /// @param liquidityChunk the chunk of liquidity to burn given by tick upper, tick lower, and its size 1167: /// @param univ3pool the Uniswap v3 pool to burn liquidity in/from 1242: // CollectedOut is the amount of fees accumulated+collected (received - burnt) 1243: // That's because receivedAmount contains the burnt tokens and whatever amount of fees collected 1244: collectedOut = int256(0).toRightSlot(collected0).toLeftSlot(collected1); 1245: 1246: _updateStoredPremia(positionKey, currentLiquidity, collectedOut);
354-355, 773, 774, 840-844, 1158, 1158, 1165-1167, 1242-1246
</details>File: contracts/tokens/ERC1155Minimal.sol 112: ERC1155Holder(to).onERC1155Received(msg.sender, from, id, amount, data) != 165: ERC1155Holder(to).onERC1155BatchReceived(msg.sender, from, ids, amounts, data) != 224: ERC1155Holder(to).onERC1155Received(msg.sender, address(0), id, amount, "") !=
Instances (13):
<details> <summary>see instances</summary>File: contracts/SemiFungiblePositionManager.sol 682: // Revert if the pool not been previously initialized 835: totalSwapped = int256(0).toRightSlot(swap0.toInt128()).toLeftSlot(swap1.toInt128()); 979: removedLiquidity -= chunkLiquidity; 1069: /// @notice caches/stores the accumulated premia values for the specified postion.
File: contracts/libraries/Math.sol 206: if (prod1 == 0) { 302: if (prod1 == 0) { 364: if (prod1 == 0) { 426: if (prod1 == 0) { 488: if (prod1 == 0) {
File: contracts/libraries/PanopticMath.sol 121: legLiquidity = Math.getLiquidityForAmount0(
</details>File: contracts/types/TokenId.sol 443: if (i == 0) 464: if (self.optionRatio(0) == 0) revert Errors.InvalidTokenIdParameter(1); 469: if (self.optionRatio(i) == 0) {
When reverting in solidity code, it is common practice to use a require or revert statement to revert execution with an error message. This can in most cases be further optimized by using assembly to revert with the error message. Instead of
require(owner == msg.sender, "caller is not owner");
use the below assembly code
assembly { if sub(caller(), sload(owner.slot)) { mstore(0x00, 0x20) // store offset to where length of revert message is stored mstore(0x20, 0x13) // store length (19) mstore(0x40,0x63616c6c6572206973206e6f74206f776e657200000000000000000000000000) // store hex representation of message revert(0x00, 0x60) // revert with data } } *Instances (6)*: ```solidity File: contracts/libraries/Math.sol 207: require(denominator > 0); 216: require(denominator > prod1); 311: require(2 ** 64 > prod1); 312: 373: require(2 ** 96 > prod1); 374: 375: /////////////////////////////////////////////// 435: require(2 ** 128 > prod1); 436: 437: /////////////////////////////////////////////// 497: require(2 ** 192 > prod1); 498: 499: ///////////////////////////////////////////////
207, 216, 311-312, 373-375, 435-437, 497-499
Inline-assembly can be used to shorten the array by changing the length slot, so that the entries don't have to be copied to a new, shorter array.
Instances (2):
File: contracts/multicall/Multicall.sol 13: results = new bytes[](data.length);
File: contracts/tokens/ERC1155Minimal.sol 182: balances = new uint256[](owners.length);
assembly
to calculate hashes to save gasUse assembly
to calculate hashes to save gas
Instances (1):
File: contracts/libraries/CallbackLib.sol 43: keccak256(abi.encode(features)),
assembly
to emit eventsWe can use assembly to emit events efficiently by utilizing scratch space
and the free memory pointer
. This will allow us to potentially avoid memory expansion costs. Note: In order to do this optimization safely, we will need to cache and restore the free memory pointer.
For example, for a generic emit event
for eventSentAmountExample
:
// uint256 id, uint256 value, uint256 amount emit eventSentAmountExample(id, value, amount);
assembly { let memptr := mload(0x40) mstore(0x00, calldataload(0x44)) mstore(0x20, calldataload(0xa4)) mstore(0x40, amount) log1( 0x00, 0x60, // keccak256("eventSentAmountExample(uint256,uint256,uint256)") 0xa622cf392588fbf2cd020ff96b2f4ebd9c76d7a4bc7f3e6b2f18012312e76bc3 ) mstore(0x40, memptr) }
Instances (8):
<details> <summary>see instances</summary>File: contracts/SemiFungiblePositionManager.sol 388: return; 490: emit TokenizedPositionBurnt(msg.sender, tokenId, positionSize); 523: emit TokenizedPositionMinted(msg.sender, tokenId, positionSize);
</details>File: contracts/tokens/ERC1155Minimal.sol 80: emit ApprovalForAll(msg.sender, operator, approved); 108: emit TransferSingle(msg.sender, from, to, id, amount); 161: emit TransferBatch(msg.sender, from, to, ids, amounts); 220: emit TransferSingle(msg.sender, address(0), to, id, amount); 239: emit TransferSingle(msg.sender, from, address(0), id, amount);
assembly
to write address storage valuesInstances (1):
File: contracts/SemiFungiblePositionManager.sol 342: constructor(IUniswapV3Factory _factory) { 343: FACTORY = _factory; 344: } 345: 346: /// @notice Initialize a Uniswap v3 pool in the SemifungiblePositionManager contract
Prior to 0.8.10 the compiler inserted extra code, including EXTCODESIZE
(100 gas), to check for contract existence for external function calls. In more recent solidity versions, the compiler will not insert these checks if the external call has a return value. Similar behavior can be achieved in earlier versions by using low-level calls, since low level calls never check for contract existence
Instances (4):
File: contracts/libraries/FeesCalc.sol /// @audit ticks() 105: unchecked { /// @audit ticks() 121: /// @audit feeGrowthGlobal0X128() 169: /// @audit feeGrowthGlobal1X128() 169:
The instances below point to the second+ call of the function within a single function.
Instances (4):
File: contracts/libraries/Math.sol /// @audit used on line 104 105: uint160 highPriceX96 = getSqrtRatioAtTick(liquidityChunk.tickUpper()); /// @audit used on line 122 123: uint160 highPriceX96 = getSqrtRatioAtTick(liquidityChunk.tickUpper()); /// @audit used on line 139 140: uint160 highPriceX96 = getSqrtRatioAtTick(liquidityChunk.tickUpper()); /// @audit used on line 158 159: uint160 highPriceX96 = getSqrtRatioAtTick(liquidityChunk.tickUpper());
Some state variables can be safely modified, and as result, the contract will use fewer storage slots. 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.
Instances (2):
File: contracts/SemiFungiblePositionManager.sol /// @audit Variable ordering with 2 slots instead of the current 3 /// IUniswapV3Factory(20) FACTORY /// bool(1) BURN /// bool(1) MINT /// uint128(16) VEGOID 2: pragma solidity =0.8.18;
File: contracts/libraries/Constants.sol /// @audit Variable ordering with 4 slots instead of the current 5 /// uint256(32) FP96 /// bytes32(32) V3POOL_INIT_CODE_HASH /// uint160(20) MAX_V3POOL_SQRT_RATIO /// int24(3) MAX_V3POOL_TICK /// int24(3) MIN_V3POOL_TICK /// uint160(20) MIN_V3POOL_SQRT_RATIO 2: pragma solidity ^0.8.0;
Not overwriting the default for storage variables avoids a Gsreset (2900 gas) during deployment
Instances (7):
File: contracts/SemiFungiblePositionManager.sol 551: registerTokenTransfer(from, to, ids[i], amounts[i]); 584: // for this leg index: extract the liquidity chunk: a 256bit word containing the liquidity amount and upper/lower tick 874: // Reverse the order of the legs if this call is burning a position (LIFO)
File: contracts/multicall/Multicall.sol 14: for (uint256 i = 0; i < data.length; ) {
File: contracts/tokens/ERC1155Minimal.sol 141: for (uint256 i = 0; i < ids.length; ) { 187: for (uint256 i = 0; i < owners.length; ++i) {
File: contracts/types/TokenId.sol 468: for (uint256 i = 0; i < 4; ++i) {
unchecked {}
can be used on the division of two uint
s in order to save gasMake such found divisions are unchecked when ensured it is safe to do so.
Instances (4):
File: contracts/SemiFungiblePositionManager.sol 1315: removedLiquidity + 1316: ((removedLiquidity ** 2) / 2 ** (VEGOID)); 1335: /// @notice Return the liquidity associated with a given position. 1336: /// @dev Computes accountLiquidity[keccak256(abi.encodePacked(univ3pool, owner, tokenType, tickLower, tickUpper))]
File: contracts/libraries/Math.sol 86: if (tick > 0) sqrtR = type(uint256).max / sqrtR; 108: mulDiv( 109: uint256(liquidityChunk.liquidity()) << 96, 110: highPriceX96 - lowPriceX96, 111: highPriceX96 112: ) / lowPriceX96;
The Solidity compiler appends 53 bytes of metadata to the smart contract code which translates to an extra 10,600 gas (200 per bytecode) + the calldata cost (16 gas per non-zero bytes, 4 gas per zero-byte). This translates to up to 848 additional gas in calldata cost. One way to reduce this cost is by optimizing the IPFS hash that gets appended to the smart contract code.
Why is this important?- The metadata adds an extra 53 bytes, resulting in an additional 10,600 gas cost for deployment.- It also incurs up to 848 additional gas in calldata cost.Options to Reduce Gas:- Use the --no-cbor-metadata
compiler option to exclude metadata, but this might affect contract verification.- Mine for code comments that lead to an IPFS hash with more zeros, reducing calldata costs.
Instances (1):
File: Various Files 1:
In Solidity, both internal functions and modifiers are used to refactor and manage code, but they come with their own trade-offs, especially in terms of gas cost and flexibility.
Modifiers:
Instances (3):
File: contracts/SemiFungiblePositionManager.sol 306: modifier ReentrancyLock(uint64 poolId) { 307: // check if the pool is already locked 477: uint256 tokenId, 478: uint128 positionSize, 479: int24 slippageTickLimitLow, 480: int24 slippageTickLimitHigh 481: ) 482: external 483: ReentrancyLock(tokenId.univ3pool()) 484: returns (int256 totalCollected, int256 totalSwapped, int24 newTick) 485: { 486: // burn this ERC1155 token id 511: uint256 tokenId, 512: uint128 positionSize, 513: int24 slippageTickLimitLow, 514: int24 slippageTickLimitHigh 515: ) 516: external 517: ReentrancyLock(tokenId.univ3pool()) 518: returns (int256 totalCollected, int256 totalSwapped, int24 newTick) 519: { 520: // create the option position via its ID in this erc1155
The Solidity compiler can generate more efficient bytecode when using named returns. It's recommended to replace anonymous returns with named returns for potential gas savings.Example:
/// 985 gas cost function add(uint256 x, uint256 y) public pure returns (uint256) { return x + y; } /// 941 gas cost function addNamed(uint256 x, uint256 y) public pure returns (uint256 res) { res = x + y; }
Instances (46):
<details> <summary>see instances</summary>File: contracts/libraries/Math.sol 23: function absUint(int256 x) internal pure returns (uint256) {
File: contracts/libraries/PanopticMath.sol 38: function getPoolId(address univ3pool) internal pure returns (uint64) { 48: function getFinalPoolId( 49: uint64 basePoolId, 50: address token0, 51: address token1, 52: uint24 fee 53: ) internal pure returns (uint64) { 145: function convert0to1(int256 amount, uint160 sqrtPriceX96) internal pure returns (int256) { 146: unchecked { 147: // the tick 443636 is the maximum price where (price) * 2**192 fits into a uint256 (< 2**256-1) 168: function convert1to0(int256 amount, uint160 sqrtPriceX96) internal pure returns (int256) { 169: unchecked { 170: // the tick 443636 is the maximum price where (price) * 2**192 fits into a uint256 (< 2**256-1)
File: contracts/tokens/ERC1155Minimal.sol 200: function supportsInterface(bytes4 interfaceId) public pure returns (bool) {
File: contracts/types/LeftRight.sol 25: function rightSlot(uint256 self) internal pure returns (uint128) { 32: function rightSlot(int256 self) internal pure returns (int128) { 44: function toRightSlot(uint256 self, uint128 right) internal pure returns (uint256) { 54: function toRightSlot(uint256 self, int128 right) internal pure returns (uint256) { 65: function toRightSlot(int256 self, uint128 right) internal pure returns (int256) { 75: function toRightSlot(int256 self, int128 right) internal pure returns (int256) { 89: function leftSlot(uint256 self) internal pure returns (uint128) { 96: function leftSlot(int256 self) internal pure returns (int128) { 108: function toLeftSlot(uint256 self, uint128 left) internal pure returns (uint256) { 118: function toLeftSlot(int256 self, uint128 left) internal pure returns (int256) { 128: function toLeftSlot(int256 self, int128 left) internal pure returns (int256) { 212: function toInt256(uint256 self) internal pure returns (int256) {
25, 32, 44, 54, 65, 75, 89, 96, 108, 118, 128, 212
File: contracts/types/LiquidityChunk.sol 68: ) internal pure returns (uint256) { 69: unchecked { 70: return self.addLiquidity(amount).addTickLower(_tickLower).addTickUpper(_tickUpper); 71: } 72: } 73: 74: /// @notice Add liquidity to the chunk. 80: return self + uint256(amount); 81: } 82: } 83: 84: /// @notice Add the lower tick to this chunk. 85: /// @param self the LiquidityChunk 90: return self + (uint256(uint24(_tickLower)) << 232); 91: } 92: } 93: 94: /// @notice Add the upper tick to this chunk. 100: // convert tick upper to uint24 as explicit conversion from int24 to uint256 is not allowed 101: return self + ((uint256(uint24(_tickUpper))) << 208); 116: } 117: 118: /// @notice Get the upper tick of a chunk. 119: /// @param self the LiquidityChunk uint256 125: } 126: 127: /// @notice Get the amount of liquidity/size of a chunk. 136:
68-74, 80-85, 90-94, 100-101, 116-119, 125-127, 136
File: contracts/types/TokenId.sol 80: function univ3pool(uint256 self) internal pure returns (uint64) { 93: function asset(uint256 self, uint256 legIndex) internal pure returns (uint256) { 103: function optionRatio(uint256 self, uint256 legIndex) internal pure returns (uint256) { 113: function isLong(uint256 self, uint256 legIndex) internal pure returns (uint256) { 123: function tokenType(uint256 self, uint256 legIndex) internal pure returns (uint256) { 139: function riskPartner(uint256 self, uint256 legIndex) internal pure returns (uint256) { 149: function strike(uint256 self, uint256 legIndex) internal pure returns (int24) { 160: function width(uint256 self, uint256 legIndex) internal pure returns (int24) { 173: function addUniv3pool(uint256 self, uint64 _poolId) internal pure returns (uint256) { 189: function addAsset( 190: uint256 self, 191: uint256 _asset, 192: uint256 legIndex 193: ) internal pure returns (uint256) { 204: function addOptionRatio( 205: uint256 self, 206: uint256 _optionRatio, 207: uint256 legIndex 208: ) internal pure returns (uint256) { 220: function addIsLong( 221: uint256 self, 222: uint256 _isLong, 223: uint256 legIndex 224: ) internal pure returns (uint256) { 234: function addTokenType( 235: uint256 self, 236: uint256 _tokenType, 237: uint256 legIndex 238: ) internal pure returns (uint256) { 248: function addRiskPartner( 249: uint256 self, 250: uint256 _riskPartner, 251: uint256 legIndex 252: ) internal pure returns (uint256) { 262: function addStrike( 263: uint256 self, 264: int24 _strike, 265: uint256 legIndex 266: ) internal pure returns (uint256) { 276: function addWidth( 277: uint256 self, 278: int24 _width, 279: uint256 legIndex 280: ) internal pure returns (uint256) { 327: function flipToBurnToken(uint256 self) internal pure returns (uint256) { 361: function countLongs(uint256 self) internal pure returns (uint256) { 410: function countLegs(uint256 self) internal pure returns (uint256) { 442: function clearLeg(uint256 self, uint256 i) internal pure returns (uint256) { 463: function validate(uint256 self) internal pure returns (uint64) {
80, 93, 103, 113, 123, 139, 149, 160, 173, 189-193, 204-208, 220-224, 234-238, 248-252, 262-266, 276-280, 327, 361, 410, 442, 463
</details>Consider changing the variable to be an unnamed one, since the variable is never assigned, nor is it returned by name. If the optimizer is not turned on, leaving the code as it is will also waste gas for the stack variable.
Instances (5):
File: contracts/SemiFungiblePositionManager.sol 1472:
File: contracts/libraries/Math.sol 101: function getAmount0ForLiquidity( 102: uint256 liquidityChunk 103: ) internal pure returns (uint256 amount0) { 119: function getAmount1ForLiquidity( 120: uint256 liquidityChunk 121: ) internal pure returns (uint256 amount1) { 135: function getLiquidityForAmount0( 136: uint256 liquidityChunk, 137: uint256 amount0 138: ) internal pure returns (uint128 liquidity) { 154: function getLiquidityForAmount1( 155: uint256 liquidityChunk, 156: uint256 amount1 157: ) internal pure returns (uint128 liquidity) {
101-103, 119-121, 135-138, 154-157
Saves 5 gas per iteration
Instances (1):
File: contracts/SemiFungiblePositionManager.sol 261: However, since we require that Eqn 2 holds up-- ie. the gross fees collected should be equal
constant
or immutable
for values set at deploymentCache such variables and perform operations on them, if operations include modifications to the state variable(s) then remember to equate the state variable to it's cached counterpart at the end
Instances (5):
File: contracts/SemiFungiblePositionManager.sol 147: mapping(address univ3pool => uint256 poolIdData) internal s_AddrToPoolIdData; 152: mapping(uint64 poolId => PoolAddressAndLock contextData) internal s_poolContext; 179: mapping(bytes32 positionKey => uint256 removedAndNetLiquidity) internal s_accountLiquidity; 288: mapping(bytes32 positionKey => uint256 accountPremium) private s_accountPremiumOwed; 290: mapping(bytes32 positionKey => uint256 accountPremium) private s_accountPremiumGross;
private
functions used once can be inlinedPrivate functions which are only called once can be inlined to save GAS.
Instances (1):
File: contracts/SemiFungiblePositionManager.sol 1092: /// @param liquidityChunk has lower tick, upper tick, and liquidity amount to mint 1093: function _getFeesBase( 1094: IUniswapV3Pool univ3pool, 1095: uint128 liquidity, 1096: uint256 liquidityChunk
The upcoming version of OpenZeppelin provides many small gas optimizations.
Instances (1):
File: package.json /// @audit path: 1: version: 4.8.3
#0 - c4-judge
2023-12-14T17:12:02Z
Picodes marked the issue as grade-b