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: 43/72
Findings: 1
Award: $19.82
🌟 Selected for report: 0
🚀 Solo Findings: 0
19.8173 USDC - $19.82
Balance operations like additions and subtractions currently use Solidity math which involves opcode gas costs.
Replace Solidity math with inline assembly for direct SLOAD and SSTORE to storage
Gas Savings:
File: contracts/tokens/ERC1155Minimal.sol function safeTransferFrom( address from, address to, uint256 id, uint256 amount, bytes calldata data ) public { if (!(msg.sender == from || isApprovedForAll[from][msg.sender])) revert NotAuthorized(); balanceOf[from][id] -= amount; // balance will never overflow unchecked { balanceOf[to][id] += amount; } afterTokenTransfer(from, to, id, amount); emit TransferSingle(msg.sender, from, to, id, amount); if (to.code.length != 0) { if ( ERC1155Holder(to).onERC1155Received(msg.sender, from, id, amount, data) != ERC1155Holder.onERC1155Received.selector ) { revert UnsafeRecipient(); } } }
function safeTransferFrom(from, to, id, amount) { // Get balances assembly { let fromBalance := sload(balanceOf, from, id) let toBalance := sload(balanceOf, to, id) } // Adjust balances assembly { sstore(balanceOf, from, id, sub(fromBalance, amount)) sstore(balanceOf, to, id, add(toBalance, amount)) } // Continue function }
ERC1155Minimal::balanceOfBatch currently returns an array of balances
Gas Savings:
File: contracts/tokens/ERC1155Minimal.sol function balanceOfBatch( address[] calldata owners, uint256[] calldata ids ) public view returns (uint256[] memory balances) { balances = new uint256[](owners.length); // Unchecked because the only math done is incrementing // the array index counter which cannot possibly overflow. unchecked { for (uint256 i = 0; i < owners.length; ++i) { balances[i] = balanceOf[owners[i]][ids[i]]; } } }
function balanceOfBatch(owners, ids) external view returns(uint256 balances) { for(uint i = 0; i < owners.length; i++) { balances |= (balancesOf[owners[i]][ids[i]] << (i * BITS_PER_BALANCE)); } return balances; } function getBalance(balances, index) internal pure returns(uint) { uint mask = (~uint256(0)) >> (BITS_PER_BALANCE * index); return (balances & mask) >> (BITS_PER_BALANCE * index); }
Bitwise operations like shifting, masking currently use Solidity opcodes.
Replace with inline assembly for direct bit manipulation.
Gas Savings:
File: contracts/types/LeftRight.sol 89 function leftSlot(uint256 self) internal pure returns (uint128) { 90 return uint128(self >> 128); 91 }
https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/types/LeftRight.sol#L89-L91
function leftSlot(x) internal pure returns (uint128) { assembly { let slot := shr(128, x) return(slot) } }
Functions like toInt256() recompute the same conversions repeatedly.
Memoize converted values in a mapping to avoid recomputation.
Gas Savings:
File: contracts/types/LeftRight.sol function toInt256(uint256 self) internal pure returns (int256) { if (self > uint256(type(int256).max)) revert Errors.CastingError(); return int256(self); }
https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/types/LeftRight.sol#L212-L215
mapping(uint256 => int256) private uint2intCache; function toInt256(uint256 x) internal view returns (int256) { if (uint2intCache[x] == 0) { uint2intCache[x] = int256(x); } return uint2intCache[x]; }
Chunk encoding currently uses addition to combine properties
Concatenate properties with bitwise OR instead of addition
Gas Savings:
File: contracts/types/LiquidityChunk.sol function createChunk( uint256 self, int24 _tickLower, int24 _tickUpper, uint128 amount ) internal pure returns (uint256) { unchecked { return self.addLiquidity(amount).addTickLower(_tickLower).addTickUpper(_tickUpper); } }
https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/types/LiquidityChunk.sol#L63-L72
function createChunk(..., tickLower, tickUpper, amount) returns (chunk) { chunk = amount; chunk |= (uint256(tickLower) << 232); chunk |= (uint256(tickUpper) << 208); return chunk; }
flipToBurnToken uses multiple comparisons to determine how many legs are active. Precompute the number of active legs instead of comparing on every call.
File: contracts/types/TokenId.sol function flipToBurnToken(uint256 self) internal pure returns (uint256) { unchecked { // NOTE: This is a hack to avoid blowing up the contract size. // We copy the logic from the countLegs function, using it here adds 5K to the contract size with IR for some reason // Strip all bits except for the option ratios uint256 optionRatios = self & OPTION_RATIO_MASK; // The legs are filled in from least to most significant // Each comparison here is to the start of the next leg's option ratio // Since only the option ratios remain, we can be sure that no bits above the start of the inactive legs will be 1 if (optionRatios < 2 ** 64) { optionRatios = 0; } else if (optionRatios < 2 ** 112) { optionRatios = 1; } else if (optionRatios < 2 ** 160) { optionRatios = 2; } else if (optionRatios < 2 ** 208) { optionRatios = 3; } else { optionRatios = 4; } // We need to ensure that only active legs are flipped // In order to achieve this, we shift our long bit mask to the right by (4-# active legs) // i.e the whole mask is used to flip all legs with 4 legs, but only the first leg is flipped with 1 leg so we shift by 3 legs // We also clear the poolId area of the mask to ensure the bits that are shifted right into the area don't flip and cause issues return self ^ ((LONG_MASK >> (48 * (4 - optionRatios))) & CLEAR_POOLID_MASK); } }
https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/types/TokenId.sol#L327-L355
// Cache number of active legs uint8 numLegs; function countLegs(uint256 tokenId) internal { // existing logic numLegs = result; } function flipToBurnToken(uint256 tokenId) internal returns (uint256) { uint256 mask = LONG_MASK; if(numLegs < 4) { mask = mask >> (48 * (4 - numLegs)); } return tokenId ^ (mask & CLEAR_POOLID_MASK); }
Avoid multiple comparisons on every call to determine active legs
#0 - c4-judge
2023-12-14T17:05:59Z
Picodes marked the issue as grade-b