Skip to content

Instantly share code, notes, and snippets.

@Picodes
Created December 12, 2022 20:20
Show Gist options
  • Save Picodes/42f9144fd8cba738f3a7098411737760 to your computer and use it in GitHub Desktop.
Save Picodes/42f9144fd8cba738f3a7098411737760 to your computer and use it in GitHub Desktop.

Report

Gas Optimizations

Issue Instances
GAS-1 Use selfbalance() instead of address(this).balance 1
GAS-2 Use assembly to check for address(0) 10
GAS-3 Cache array length outside of loop 3
GAS-4 Use calldata instead of memory for function arguments that do not get mutated 4
GAS-5 Use Custom Errors 16
GAS-6 Don't initialize variables with default value 8
GAS-7 ++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too) 8
GAS-8 Using private rather than public for constants, saves gas 2
GAS-9 Use != 0 instead of > 0 for unsigned integer comparison 4
GAS-10 internal functions not called by the contract should be removed 2

[GAS-1] Use selfbalance() instead of address(this).balance

Use assembly when getting a contract's balance of ETH.

You can use selfbalance() instead of address(this).balance when getting your contract's balance of ETH to save gas. Additionally, you can use balance(address) instead of address.balance() when getting an external contract's balance of ETH.

Saves 15 gas when checking internal balance, 6 for external

Instances (1):

File: src/Pair.sol

479:             ? address(this).balance - msg.value // subtract the msg.value if the base token is ETH

Link to code

[GAS-2] Use assembly to check for address(0)

Saves 6 gas per instance

Instances (10):

File: src/Caviar.sol

30:         require(pairs[nft][baseToken][merkleRoot] == address(0), "Pair already exists");

33:         string memory baseTokenSymbol = baseToken == address(0) ? "ETH" : baseToken.tokenSymbol();

Link to code

File: src/Pair.sol

74:         require(baseToken == address(0) ? msg.value == baseTokenAmount : msg.value == 0, "Invalid ether input");

93:         if (baseToken != address(0)) {

132:         if (baseToken == address(0)) {

151:         require(baseToken == address(0) ? msg.value == maxInputAmount : msg.value == 0, "Invalid ether input");

166:         if (baseToken == address(0)) {

198:         if (baseToken == address(0)) {

465:         if (merkleRoot == bytes23(0)) return;

478:         return baseToken == address(0)

Link to code

[GAS-3] Cache array length outside of loop

If not cached, the solidity compiler will always read the length of the array during each iteration. That is, if it is a storage array, this is an extra sload operation (100 additional extra gas for each iteration except for the first) and if it is a memory array, this is an extra mload operation (3 additional gas for each iteration except for the first).

Instances (3):

File: src/Pair.sol

238:         for (uint256 i = 0; i < tokenIds.length; i++) {

258:         for (uint256 i = 0; i < tokenIds.length; i++) {

468:         for (uint256 i = 0; i < tokenIds.length; i++) {

Link to code

[GAS-4] Use calldata instead of memory for function arguments that do not get mutated

Mark data types as calldata instead of memory where possible. This makes it so that the data is not automatically loaded into memory. If the data passed into the function does not need to be changed (like updating values in an array), it can be passed in as calldata. The one exception to this is if the argument must later be passed into another function that takes an argument that specifies memory storage.

Instances (4):

File: src/LpToken.sol

11:     constructor(string memory pairSymbol)

Link to code

File: src/Pair.sol

43:         string memory pairSymbol,

44:         string memory nftName,

45:         string memory nftSymbol

Link to code

[GAS-5] Use Custom Errors

Source Instead of using error strings, to reduce deployment and runtime cost, you should use Custom Errors. This would save both deployment and runtime cost.

Instances (16):

File: src/Caviar.sol

30:         require(pairs[nft][baseToken][merkleRoot] == address(0), "Pair already exists");

51:         require(msg.sender == pairs[nft][baseToken][merkleRoot], "Only pair can destroy itself");

Link to code

File: src/Pair.sol

71:         require(baseTokenAmount > 0 && fractionalTokenAmount > 0, "Input token amount is zero");

74:         require(baseToken == address(0) ? msg.value == baseTokenAmount : msg.value == 0, "Invalid ether input");

80:         require(lpTokenAmount >= minLpTokenAmount, "Slippage: lp token amount out");

117:         require(baseTokenOutputAmount >= minBaseTokenOutputAmount, "Slippage: base token amount out");

120:         require(fractionalTokenOutputAmount >= minFractionalTokenOutputAmount, "Slippage: fractional token out");

151:         require(baseToken == address(0) ? msg.value == maxInputAmount : msg.value == 0, "Invalid ether input");

157:         require(inputAmount <= maxInputAmount, "Slippage: amount in");

189:         require(outputAmount >= minOutputAmount, "Slippage: amount out");

224:         require(closeTimestamp == 0, "Wrap: closed");

343:         require(caviar.owner() == msg.sender, "Close: not owner");

361:         require(caviar.owner() == msg.sender, "Withdraw: not owner");

364:         require(closeTimestamp != 0, "Withdraw not initiated");

367:         require(block.timestamp >= closeTimestamp, "Not withdrawable yet");

470:             require(isValid, "Invalid merkle proof");

Link to code

[GAS-6] Don't initialize variables with default value

Instances (8):

File: src/Pair.sol

238:         for (uint256 i = 0; i < tokenIds.length; i++) {

258:         for (uint256 i = 0; i < tokenIds.length; i++) {

468:         for (uint256 i = 0; i < tokenIds.length; i++) {

Link to code

File: src/lib/SafeERC20Namer.sol

12:         uint256 charCount = 0;

13:         for (uint256 j = 0; j < 32; j++) {

22:         for (uint256 j = 0; j < charCount; j++) {

31:         uint256 charCount = 0;

39:         for (uint256 i = 0; i < charCount; i++) {

Link to code

[GAS-7] ++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too)

Saves 5 gas per loop

Instances (8):

File: src/Pair.sol

238:         for (uint256 i = 0; i < tokenIds.length; i++) {

258:         for (uint256 i = 0; i < tokenIds.length; i++) {

468:         for (uint256 i = 0; i < tokenIds.length; i++) {

Link to code

File: src/lib/SafeERC20Namer.sol

13:         for (uint256 j = 0; j < 32; j++) {

17:                 charCount++;

22:         for (uint256 j = 0; j < charCount; j++) {

33:         for (uint256 i = 32; i < 64; i++) {

39:         for (uint256 i = 0; i < charCount; i++) {

Link to code

[GAS-8] Using private rather than public for constants, saves gas

If needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table

Instances (2):

File: src/Pair.sol

20:     uint256 public constant ONE = 1e18;

21:     uint256 public constant CLOSE_GRACE_PERIOD = 7 days;

Link to code

[GAS-9] Use != 0 instead of > 0 for unsigned integer comparison

Instances (4):

File: src/Pair.sol

71:         require(baseTokenAmount > 0 && fractionalTokenAmount > 0, "Input token amount is zero");

71:         require(baseTokenAmount > 0 && fractionalTokenAmount > 0, "Input token amount is zero");

169:             if (refundAmount > 0) msg.sender.safeTransferETH(refundAmount);

419:         if (lpTokenSupply > 0) {

Link to code

[GAS-10] internal functions not called by the contract should be removed

If the functions are required by an interface, the contract should inherit from that interface and use the override keyword

Instances (2):

File: src/lib/SafeERC20Namer.sol

76:     function tokenSymbol(address token) internal view returns (string memory) {

87:     function tokenName(address token) internal view returns (string memory) {

Link to code

Non Critical Issues

Issue Instances
NC-1 Missing checks for address(0) when assigning values to address state variables 2
NC-2 Event is missing indexed fields 8
NC-3 Constants should be defined rather than using magic numbers 1
NC-4 Functions not used internally could be marked external 11

[NC-1] Missing checks for address(0) when assigning values to address state variables

Instances (2):

File: src/Pair.sol

47:         nft = _nft;

48:         baseToken = _baseToken; // use address(0) for native ETH

Link to code

[NC-2] Event is missing indexed fields

Index event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event should use three indexed fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.

Instances (8):

File: src/Pair.sol

30:     event Add(uint256 baseTokenAmount, uint256 fractionalTokenAmount, uint256 lpTokenAmount);

31:     event Remove(uint256 baseTokenAmount, uint256 fractionalTokenAmount, uint256 lpTokenAmount);

32:     event Buy(uint256 inputAmount, uint256 outputAmount);

33:     event Sell(uint256 inputAmount, uint256 outputAmount);

34:     event Wrap(uint256[] tokenIds);

35:     event Unwrap(uint256[] tokenIds);

36:     event Close(uint256 closeTimestamp);

37:     event Withdraw(uint256 tokenId);

Link to code

[NC-3] Constants should be defined rather than using magic numbers

Instances (1):

File: src/lib/SafeERC20Namer.sol

55:         return Strings.toHexString(uint160(token) >> (160 - 4 * 4));

Link to code

[NC-4] Functions not used internally could be marked external

Instances (11):

File: src/Caviar.sol

28:     function create(address nft, address baseToken, bytes32 merkleRoot) public returns (Pair pair) {

49:     function destroy(address nft, address baseToken, bytes32 merkleRoot) public {

Link to code

File: src/LpToken.sol

19:     function mint(address to, uint256 amount) public onlyOwner {

26:     function burn(address from, uint256 amount) public onlyOwner {

Link to code

File: src/Pair.sol

275:     function nftAdd(

294:     function nftRemove(uint256 lpTokenAmount, uint256 minBaseTokenOutputAmount, uint256[] calldata tokenIds)

310:     function nftBuy(uint256[] calldata tokenIds, uint256 maxInputAmount) public payable returns (uint256 inputAmount) {

323:     function nftSell(uint256[] calldata tokenIds, uint256 minOutputAmount, bytes32[][] calldata proofs)

341:     function close() public {

359:     function withdraw(uint256 tokenId) public {

390:     function price() public view returns (uint256) {

Link to code

Low Issues

Issue Instances
L-1 abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256() 1

[L-1] abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()

Use abi.encode() instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456) => 0x123456 => abi.encodePacked(0x1,0x23456), but abi.encode(0x123,0x456) => 0x0...1230...456). "Unless there is a compelling reason, abi.encode should be preferred". If there is only one argument to abi.encodePacked() it can often be cast to bytes() or bytes32() instead. If all arguments are strings and or bytes, bytes.concat() should be used instead

Instances (1):

File: src/Pair.sol

469:             bool isValid = MerkleProofLib.verify(proofs[i], merkleRoot, keccak256(abi.encodePacked(tokenIds[i])));

Link to code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment