Numoen contest - IllIllI's results

Automated exchange for power perpetuals.

General Information

Platform: Code4rena

Start Date: 26/01/2023

Pot Size: $60,500 USDC

Total HM: 7

Participants: 31

Period: 6 days

Judge: berndartmueller

Total Solo HM: 3

Id: 207

League: ETH

Numoen

Findings Distribution

Researcher Performance

Rank: 8/31

Findings: 2

Award: $1,548.21

QA:
grade-a
Gas:
grade-a

🌟 Selected for report: 0

šŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: CodingNameKiki

Also found by: 0xAgro, 0xSmartContract, IllIllI, Rolezn, SleepingBugs, btk, chrisdior4, matrix_0wl

Labels

bug
grade-a
QA (Quality Assurance)
sponsor confirmed
Q-04

Awards

997.1116 USDC - $997.11

External Links

Summary

Low Risk Issues

IssueInstances
[L‑01]Loss of precision1
[L‑02]abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()1

Total: 2 instances over 2 issues

Non-critical Issues

IssueInstances
[N‑01]2**<n> - 1 should be re-written as type(uint<n>).max1
[N‑02]constants should be defined rather than using magic numbers27
[N‑03]Use a more recent version of solidity1
[N‑04]Use a more recent version of solidity2
[N‑05]Inconsistent method of specifying a floating pragma1
[N‑06]Non-library/interface files should use fixed compiler versions, not floating ones4
[N‑07]Using >/>= without specifying an upper bound is unsafe3
[N‑08]Typos1
[N‑09]File does not contain an SPDX Identifier1
[N‑10]Misplaced SPDX identifier1
[N‑11]File is missing NatSpec3
[N‑12]NatSpec is incomplete3
[N‑13]Not using the named return variables anywhere in the function is confusing2
[N‑14]Duplicated require()/revert() checks should be refactored to a modifier or function1
[N‑15]Contracts should have full test coverage1
[N‑16]Large or complicated code bases should implement fuzzing tests1
[N‑17]Function ordering does not follow the Solidity style guide3
[N‑18]Contract does not follow the Solidity style guide's suggested layout ordering8

Total: 64 instances over 18 issues

Low Risk Issues

[L‑01] Loss of precision

Division by large numbers may result in the result being zero, due to solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator

There is 1 instance of this issue:

File: src/core/JumpRate.sol

42:       return (borrowedLiquidity * 1e18) / totalLiquidity;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/JumpRate.sol#L42

[L‑02] 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

There is 1 instance of this issue:

File: src/periphery/libraries/LendgineAddress.sol

24              keccak256(
25                abi.encodePacked(
26                  hex"ff",
27                  factory,
28                  keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)),
29                  bytes32(INIT_CODE_HASH)
30                )
31:             )

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/libraries/LendgineAddress.sol#L24-L31

Non-critical Issues

[N‑01] 2**<n> - 1 should be re-written as type(uint<n>).max

Earlier versions of solidity can use uint<n>(-1) instead. Expressions not including the - 1 can often be re-written to accomodate the change (e.g. by using a > rather than a >=, which will also save some gas)

There is 1 instance of this issue:

File: src/libraries/SafeCast.sol

16:       require(y < 2 ** 255);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/libraries/SafeCast.sol#L16

[N‑02] constants should be defined rather than using magic numbers

Even assembly can benefit from using readable constants instead of hex/numeric literals

There are 27 instances of this issue:

File: src/core/Factory.sol

/// @audit 18
/// @audit 6
/// @audit 18
/// @audit 6
77:       if (token0Exp > 18 || token0Exp < 6 || token1Exp > 18 || token1Exp < 6) revert ScaleError();

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Factory.sol#L77

File: src/core/JumpRate.sol

/// @audit 1e18
17:         return (util * multiplier) / 1e18;

/// @audit 1e18
19:         uint256 normalRate = (kink * multiplier) / 1e18;

/// @audit 1e18
21:         return ((excessUtil * jumpMultiplier) / 1e18) + normalRate;

/// @audit 1e18
37:       return (borrowRate * util) / 1e18;

/// @audit 1e18
42:       return (borrowedLiquidity * 1e18) / totalLiquidity;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/JumpRate.sol#L17

File: src/core/Lendgine.sol

/// @audit 1e18
225:      return FullMath.mulDiv(collateral * token1Scale, 1e18, 2 * upperBound);

/// @audit 1e18
230:      return FullMath.mulDiv(liquidity, 2 * upperBound, 1e18) / token1Scale;

/// @audit 1e18
/// @audit 365
252:      uint256 dilutionLPRequested = (FullMath.mulDiv(borrowRate, _totalLiquidityBorrowed, 1e18) * timeElapsed) / 365 days;

/// @audit 1e18
257:      rewardPerPositionStored += FullMath.mulDiv(dilutionSpeculative, 1e18, totalPositionSize);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L225

File: src/core/Pair.sol

/// @audit 1e18
56:       uint256 scale0 = FullMath.mulDiv(amount0, 1e18, liquidity) * token0Scale;

/// @audit 1e18
57:       uint256 scale1 = FullMath.mulDiv(amount1, 1e18, liquidity) * token1Scale;

/// @audit 1e18
61:       uint256 a = scale0 * 1e18;

/// @audit 4
63:       uint256 c = (scale1 * scale1) / 4;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Pair.sol#L56

File: src/libraries/Balance.sol

/// @audit 32
15:       if (!success || data.length < 32) revert BalanceReturnError();

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/libraries/Balance.sol#L15

File: src/libraries/SafeCast.sol

/// @audit 255
16:       require(y < 2 ** 255);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/libraries/SafeCast.sol#L16

File: src/periphery/LiquidityManager.sol

/// @audit 1e18
178:      position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18);

/// @audit 1e18
214:      position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18);

/// @audit 1e18
238:      position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L178

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

/// @audit 997
62:       uint256 amountInWithFee = amountIn * 997;

/// @audit 1000
64:       uint256 denominator = (reserveIn * 1000) + amountInWithFee;

/// @audit 1000
80:       uint256 numerator = reserveIn * amountOut * 1000;

/// @audit 997
81:       uint256 denominator = (reserveOut - amountOut) * 997;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L62

[N‑03] Use a more recent version of solidity

Use a solidity version of at least 0.8.13 to get the ability to use using for with a list of free functions

There is 1 instance of this issue:

File: src/core/Lendgine.sol

2:    pragma solidity ^0.8.4;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L2

[N‑04] Use a more recent version of solidity

Use a solidity version of at least 0.8.4 to get bytes.concat() instead of abi.encodePacked(<bytes>,<bytes>) Use a solidity version of at least 0.8.12 to get string.concat() instead of abi.encodePacked(<str>,<str>)

There are 2 instances of this issue:

File: src/periphery/libraries/LendgineAddress.sol

2:    pragma solidity >=0.5.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/libraries/LendgineAddress.sol#L2

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

1:    pragma solidity >=0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L1

[N‑05] Inconsistent method of specifying a floating pragma

Some files use >=, some use ^. The instances below are examples of the method that has the fewest instances for a specific version. Note that using >= without also specifying <= will lead to failures to compile, or external project incompatability, when the major version changes and there are breaking-changes, so ^ should be preferred regardless of the instance counts

There is 1 instance of this issue:

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

1:    pragma solidity >=0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L1

[N‑06] Non-library/interface files should use fixed compiler versions, not floating ones

There are 4 instances of this issue:

File: src/core/Factory.sol

2:    pragma solidity ^0.8.4;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Factory.sol#L2

File: src/core/Lendgine.sol

2:    pragma solidity ^0.8.4;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L2

File: src/periphery/LendgineRouter.sol

2:    pragma solidity ^0.8.4;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LendgineRouter.sol#L2

File: src/periphery/LiquidityManager.sol

2:    pragma solidity ^0.8.4;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L2

[N‑07] Using >/>= without specifying an upper bound is unsafe

There will be breaking changes in future versions of solidity, and at that point your code will no longer be compatable. While you may have the specific version to use in a configuration file, others that include your source files may not.

There are 3 instances of this issue:

File: src/core/libraries/PositionMath.sol

2:    pragma solidity >=0.5.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/libraries/PositionMath.sol#L2

File: src/periphery/libraries/LendgineAddress.sol

2:    pragma solidity >=0.5.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/libraries/LendgineAddress.sol#L2

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

1:    pragma solidity >=0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L1

[N‑08] Typos

There is 1 instance of this issue:

File: src/periphery/LiquidityManager.sol

/// @audit liqudity
229:    /// @notice Collects interest owed to the callers liqudity position

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L229

[N‑09] File does not contain an SPDX Identifier

There is 1 instance of this issue:

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

0:    pragma solidity >=0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L0

[N‑10] Misplaced SPDX identifier

The SPDX identifier should be on the very first line of the file

There is 1 instance of this issue:

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

1:    pragma solidity >=0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L1

[N‑11] File is missing NatSpec

There are 3 instances of this issue:

File: src/core/Factory.sol

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Factory.sol

File: src/core/ImmutableState.sol

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/ImmutableState.sol

File: src/core/JumpRate.sol

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/JumpRate.sol

[N‑12] NatSpec is incomplete

There are 3 instances of this issue:

File: src/core/libraries/Position.sol

/// @audit Missing: '@param position'
/// @audit Missing: '@return'
67      /// @notice Helper function for determining the amount of tokens owed to a position
68      /// @param rewardPerPosition The global accrued interest
69:     function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/libraries/Position.sol#L67-L69

File: src/periphery/SwapHelper.sol

/// @audit Missing: '@param params'
65      /// @notice Handles swaps on Uniswap V2 or V3
66      /// @param swapType A selector for UniswapV2 or V3
67      /// @param data Extra data that is not used by all types of swaps
68      /// @return amount The amount in or amount out depending on whether the call was exact in or exact out
69:     function swap(SwapType swapType, SwapParams memory params, bytes memory data) internal returns (uint256 amount) {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/SwapHelper.sol#L65-L69

[N‑13] Not using the named return variables anywhere in the function is confusing

Consider changing the variable to be an unnamed one

There are 2 instances of this issue:

File: src/core/JumpRate.sol

/// @audit rate
25      function getSupplyRate(
26        uint256 borrowedLiquidity,
27        uint256 totalLiquidity
28      )
29        external
30        pure
31        override
32:       returns (uint256 rate)

/// @audit rate
40:     function utilizationRate(uint256 borrowedLiquidity, uint256 totalLiquidity) private pure returns (uint256 rate) {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/JumpRate.sol#L25-L32

[N‑14] Duplicated require()/revert() checks should be refactored to a modifier or function

The compiler will inline the function, which will avoid JUMP instructions usually associated with functions

There is 1 instance of this issue:

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

79:       require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L79

[N‑15] Contracts should have full test coverage

While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.

There is 1 instance of this issue:

File: Various Files

[N‑16] Large or complicated code bases should implement fuzzing tests

Large code bases, or code with lots of inline-assembly, complicated math, or complicated interactions between multiple contracts, should implement fuzzing tests. Fuzzers such as Echidna require the test writer to come up with invariants which should not be violated under any circumstances, and the fuzzer tests various inputs and function calls to ensure that the invariants always hold. Even code with 100% code coverage can still have bugs due to the order of the operations a user performs, and fuzzers, with properly and extensively-written invariants, can close this testing gap significantly.

There is 1 instance of this issue:

File: Various Files

[N‑17] Function ordering does not follow the Solidity style guide

According to the Solidity style guide, functions should be laid out in the following order :constructor(), receive(), fallback(), external, public, internal, private, but the cases below do not follow this pattern

There are 3 instances of this issue:

File: src/core/JumpRate.sol

/// @audit getBorrowRate() came earlier
25      function getSupplyRate(
26        uint256 borrowedLiquidity,
27        uint256 totalLiquidity
28      )
29        external
30        pure
31        override
32:       returns (uint256 rate)

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/JumpRate.sol#L25-L32

File: src/core/Pair.sol

/// @audit burn() came earlier
116:    function swap(address to, uint256 amount0Out, uint256 amount1Out, bytes calldata data) external override nonReentrant {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Pair.sol#L116

File: src/periphery/Payment.sol

/// @audit sweepToken() came earlier
44      function refundETH() external payable {
45:       if (address(this).balance > 0) SafeTransferLib.safeTransferETH(msg.sender, address(this).balance);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/Payment.sol#L44-L45

[N‑18] Contract does not follow the Solidity style guide's suggested layout ordering

The style guide says that, within a contract, the ordering should be 1) Type declarations, 2) State variables, 3) Events, 4) Modifiers, and 5) Functions, but the contract(s) below do not follow this ordering

There are 8 instances of this issue:

File: src/core/Factory.sol

/// @audit event LendgineCreated came earlier
39      mapping(address => mapping(address => mapping(uint256 => mapping(uint256 => mapping(uint256 => address)))))
40        public
41:       override getLendgine;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Factory.sol#L39-L41

File: src/core/Lendgine.sol

/// @audit event Collect came earlier
56:     mapping(address => Position.Info) public override positions;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L56

File: src/core/Pair.sol

/// @audit event Swap came earlier
40:     uint120 public override reserve0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Pair.sol#L40

File: src/periphery/LendgineRouter.sol

/// @audit event Burn came earlier
43:     address public immutable factory;

/// @audit function constructor came earlier
65      modifier checkDeadline(uint256 deadline) {
66        if (deadline < block.timestamp) revert LivelinessError();
67        _;
68:     }

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LendgineRouter.sol#L43

File: src/periphery/LiquidityManager.sol

/// @audit event Collect came earlier
60:     address public immutable factory;

/// @audit function constructor came earlier
83      modifier checkDeadline(uint256 deadline) {
84        if (deadline < block.timestamp) revert LivelinessError();
85        _;
86:     }

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L60

File: src/periphery/SwapHelper.sol

/// @audit function uniswapV3SwapCallback came earlier
49      enum SwapType {
50        UniswapV2,
51        UniswapV3
52:     }

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/SwapHelper.sol#L49-L52


Excluded findings

These findings are excluded from awards calculations because there are publicly-available automated tools that find them. The valid ones appear here for completeness

Summary

Low Risk Issues

IssueInstances
[L‑01]Missing checks for address(0x0) when assigning values to address state variables5
[L‑02]abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()1

Total: 6 instances over 2 issues

Non-critical Issues

IssueInstances
[N‑01]require()/revert() statements should have descriptive reason strings3
[N‑02]constants should be defined rather than using magic numbers2
[N‑03]Event is missing indexed fields10

Total: 15 instances over 3 issues

Low Risk Issues

[L‑01] Missing checks for address(0x0) when assigning values to address state variables

There are 5 instances of this issue:

File: src/periphery/LendgineRouter.sol

/// @audit (valid but excluded finding)
58:       factory = _factory;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LendgineRouter.sol#L58

File: src/periphery/LiquidityManager.sol

/// @audit (valid but excluded finding)
76:       factory = _factory;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L76

File: src/periphery/Payment.sol

/// @audit (valid but excluded finding)
18:       weth = _weth;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/Payment.sol#L18

File: src/periphery/SwapHelper.sol

/// @audit (valid but excluded finding)
30:       uniswapV2Factory = _uniswapV2Factory;

/// @audit (valid but excluded finding)
31:       uniswapV3Factory = _uniswapV3Factory;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/SwapHelper.sol#L30

[L‑02] 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

There is 1 instance of this issue:

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

/// @audit (valid but excluded finding)
22              keccak256(
23                abi.encodePacked(
24                  hex"ff",
25                  factory,
26                  keccak256(abi.encodePacked(token0, token1)),
27                  hex"e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" // init code hash
28                )
29:             )

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L22-L29

Non-critical Issues

[N‑01] require()/revert() statements should have descriptive reason strings

There are 3 instances of this issue:

File: src/libraries/SafeCast.sol

/// @audit (valid but excluded finding)
9:        require((z = uint120(y)) == y);

/// @audit (valid but excluded finding)
16:       require(y < 2 ** 255);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/libraries/SafeCast.sol#L9

File: src/periphery/SwapHelper.sol

/// @audit (valid but excluded finding)
116:          require(amountOutReceived == params.amount);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/SwapHelper.sol#L116

[N‑02] constants should be defined rather than using magic numbers

Even assembly can benefit from using readable constants instead of hex/numeric literals

There are 2 instances of this issue:

File: src/core/ImmutableState.sol

/// @audit 18 - (valid but excluded finding)
35:       token0Scale = 10 ** (18 - _token0Exp);

/// @audit 18 - (valid but excluded finding)
36:       token1Scale = 10 ** (18 - _token1Exp);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/ImmutableState.sol#L35

[N‑03] 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.

There are 10 instances of this issue:

File: src/core/Lendgine.sol

/// @audit (valid but excluded finding)
25:     event Mint(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to);

/// @audit (valid but excluded finding)
27:     event Burn(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to);

/// @audit (valid but excluded finding)
29:     event Deposit(address indexed sender, uint256 size, uint256 liquidity, address indexed to);

/// @audit (valid but excluded finding)
31:     event Withdraw(address indexed sender, uint256 size, uint256 liquidity, address indexed to);

/// @audit (valid but excluded finding)
33:     event AccrueInterest(uint256 timeElapsed, uint256 collateral, uint256 liquidity);

/// @audit (valid but excluded finding)
35:     event AccruePositionInterest(address indexed owner, uint256 rewardPerPosition);

/// @audit (valid but excluded finding)
37:     event Collect(address indexed owner, address indexed to, uint256 amount);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L25

File: src/core/Pair.sol

/// @audit (valid but excluded finding)
21:     event Mint(uint256 amount0In, uint256 amount1In, uint256 liquidity);

/// @audit (valid but excluded finding)
23:     event Burn(uint256 amount0Out, uint256 amount1Out, uint256 liquidity, address indexed to);

/// @audit (valid but excluded finding)
25:     event Swap(uint256 amount0Out, uint256 amount1Out, uint256 amount0In, uint256 amount1In, address indexed to);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Pair.sol#L21

#0 - c4-judge

2023-02-08T14:48:00Z

berndartmueller marked the issue as grade-a

#1 - c4-sponsor

2023-02-09T16:46:21Z

kyscott18 marked the issue as sponsor confirmed

#2 - kyscott18

2023-02-09T16:53:16Z

The issues here are very helpful but I am not going to change anything in the codebase unless explicitly necessary.

Awards

551.0959 USDC - $551.10

Labels

bug
G (Gas Optimization)
grade-a
G-08

External Links

Summary

Gas Optimizations

IssueInstancesTotal Gas Saved
[G‑01]Structs can be packed into fewer storage slots4-
[G‑02]Using storage instead of memory for structs/arrays saves gas312600
[G‑03]Avoid contract existence checks by using low level calls202000
[G‑04]State variables should be cached in stack variables rather than re-reading them from storage2194
[G‑05]<x> += <y> costs more gas than <x> = <x> + <y> for state variables4452
[G‑06]Optimize names to save gas366
[G‑07]Use a more recent version of solidity2-
[G‑08]Use a more recent version of solidity4-
[G‑09]>= costs less gas than >39
[G‑10]Splitting require() statements that use && saves gas26
[G‑11]Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead7-
[G‑12]Stack variable used as a cheaper cache for a state variable is only used once13
[G‑13]Multiple if-statements with mutually-exclusive conditions should be changed to if-else statements1-
[G‑14]Constructors can be marked payable5-

Total: 61 instances over 14 issues with 15330 gas saved

Gas totals use lower bounds of ranges and count two iterations of each for-loop. All values above are runtime, not deployment, values; deployment values are listed in the individual issue descriptions. The table above as well as its gas numbers do not include any of the excluded findings.

Gas Optimizations

[G‑01] Structs can be packed into 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

There are 4 instances of this issue:

File: src/periphery/LendgineRouter.sol

/// @audit Variable ordering with 8 slots instead of the current 9:
///           uint256(32):token0Exp, uint256(32):token1Exp, uint256(32):upperBound, uint256(32):collateralMax, bytes(32):swapExtraData, address(20):token0, uint8(1):swapType, address(20):token1, address(20):payer
74      struct MintCallbackData {
75        address token0;
76        address token1;
77        uint256 token0Exp;
78        uint256 token1Exp;
79        uint256 upperBound;
80        uint256 collateralMax;
81        SwapType swapType;
82        bytes swapExtraData;
83        address payer;
84:     }

/// @audit Variable ordering with 11 slots instead of the current 12:
///           uint256(32):token0Exp, uint256(32):token1Exp, uint256(32):upperBound, uint256(32):amountIn, uint256(32):amountBorrow, uint256(32):sharesMin, bytes(32):swapExtraData, uint256(32):deadline, address(20):token0, uint8(1):swapType, address(20):token1, address(20):recipient
126     struct MintParams {
127       address token0;
128       address token1;
129       uint256 token0Exp;
130       uint256 token1Exp;
131       uint256 upperBound;
132       uint256 amountIn;
133       uint256 amountBorrow;
134       uint256 sharesMin;
135       SwapType swapType;
136       bytes swapExtraData;
137       address recipient;
138       uint256 deadline;
139:    }

/// @audit Variable ordering with 10 slots instead of the current 11:
///           uint256(32):token0Exp, uint256(32):token1Exp, uint256(32):upperBound, uint256(32):collateralMin, uint256(32):amount0Min, uint256(32):amount1Min, bytes(32):swapExtraData, address(20):token0, uint8(1):swapType, address(20):token1, address(20):recipient
175     struct PairMintCallbackData {
176       address token0;
177       address token1;
178       uint256 token0Exp;
179       uint256 token1Exp;
180       uint256 upperBound;
181       uint256 collateralMin;
182       uint256 amount0Min;
183       uint256 amount1Min;
184       SwapType swapType;
185       bytes swapExtraData;
186       address recipient;
187:    }

/// @audit Variable ordering with 12 slots instead of the current 13:
///           uint256(32):token0Exp, uint256(32):token1Exp, uint256(32):upperBound, uint256(32):shares, uint256(32):collateralMin, uint256(32):amount0Min, uint256(32):amount1Min, bytes(32):swapExtraData, uint256(32):deadline, address(20):token0, uint8(1):swapType, address(20):token1, address(20):recipient
240     struct BurnParams {
241       address token0;
242       address token1;
243       uint256 token0Exp;
244       uint256 token1Exp;
245       uint256 upperBound;
246       uint256 shares;
247       uint256 collateralMin;
248       uint256 amount0Min;
249       uint256 amount1Min;
250       SwapType swapType;
251       bytes swapExtraData;
252       address recipient;
253       uint256 deadline;
254:    }

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LendgineRouter.sol#L74-L84

[G‑02] Using storage instead of memory for structs/arrays saves gas

When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct

There are 3 instances of this issue:

File: src/core/Lendgine.sol

167:      Position.Info memory positionInfo = positions[msg.sender]; // SLOAD

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L167

File: src/core/libraries/Position.sol

47:       Position.Info memory _positionInfo = positionInfo;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/libraries/Position.sol#L47

File: src/periphery/LiquidityManager.sol

211:      Position memory position = positions[msg.sender][lendgine]; // SLOAD

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L211

[G‑03] Avoid contract existence checks by using low level calls

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

There are 20 instances of this issue:

File: src/core/ImmutableState.sol

/// @audit parameters()
33:       (token0, token1, _token0Exp, _token1Exp, upperBound) = Factory(msg.sender).parameters();

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/ImmutableState.sol#L33

File: src/core/Pair.sol

/// @audit swapCallback()
127:      ISwapCallback(msg.sender).swapCallback(amount0Out, amount1Out, data);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Pair.sol#L127

File: src/periphery/LendgineRouter.sol

/// @audit mint()
147:      shares = ILendgine(lendgine).mint(

/// @audit reserve0()
198:      uint256 r0 = ILendgine(msg.sender).reserve0();

/// @audit reserve1()
199:      uint256 r1 = ILendgine(msg.sender).reserve1();

/// @audit totalLiquidity()
200:      uint256 totalLiquidity = ILendgine(msg.sender).totalLiquidity();

/// @audit convertLiquidityToCollateral()
231:      uint256 collateralTotal = ILendgine(msg.sender).convertLiquidityToCollateral(liquidity);

/// @audit burn()
266:      amount = ILendgine(lendgine).burn(

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LendgineRouter.sol#L147

File: src/periphery/LiquidityManager.sol

/// @audit reserve0()
140:      uint256 r0 = ILendgine(lendgine).reserve0();

/// @audit reserve1()
141:      uint256 r1 = ILendgine(lendgine).reserve1();

/// @audit totalLiquidity()
142:      uint256 totalLiquidity = ILendgine(lendgine).totalLiquidity();

/// @audit deposit()
157:      uint256 size = ILendgine(lendgine).deposit(

/// @audit positions()
177:      (, uint256 rewardPerPositionPaid,) = ILendgine(lendgine).positions(address(this));

/// @audit withdraw()
208:      (uint256 amount0, uint256 amount1, uint256 liquidity) = ILendgine(lendgine).withdraw(recipient, params.size);

/// @audit positions()
213:      (, uint256 rewardPerPositionPaid,) = ILendgine(lendgine).positions(address(this));

/// @audit positions()
237:      (, uint256 rewardPerPositionPaid,) = ILendgine(params.lendgine).positions(address(this));

/// @audit collect()
246:      uint256 collectAmount = ILendgine(params.lendgine).collect(recipient, amount);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L140

File: src/periphery/Payment.sol

/// @audit withdraw()
30:         IWETH9(weth).withdraw(balanceWETH);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/Payment.sol#L30

File: src/periphery/SwapHelper.sol

/// @audit swap()
88:         IUniswapV2Pair(pair).swap(amount0Out, amount1Out, params.recipient, bytes(""));

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/SwapHelper.sol#L88

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

/// @audit getReserves()
46:       (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L46

[G‑04] State variables should be cached in stack variables rather than re-reading them from storage

The instances below point to the second+ access of a state variable within a function. Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.

There are 2 instances of this issue:

File: src/core/Lendgine.sol

/// @audit totalPositionSize on line 135
142:      if (totalLiquiditySupplied == 0 && totalPositionSize > 0) revert CompleteUtilizationError();

/// @audit totalLiquidityBorrowed on line 239
247:      uint256 _totalLiquidityBorrowed = totalLiquidityBorrowed; // SLOAD

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L142

[G‑05] <x> += <y> costs more gas than <x> = <x> + <y> for state variables

Using the addition operator instead of plus-equals saves 113 gas

There are 4 instances of this issue:

File: src/core/Lendgine.sol

91:       totalLiquidityBorrowed += liquidity;

114:      totalLiquidityBorrowed -= liquidity;

176:      totalPositionSize -= size;

257:      rewardPerPositionStored += FullMath.mulDiv(dilutionSpeculative, 1e18, totalPositionSize);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L91

[G‑06] Optimize names to save gas

public/external function names and public member variable names can be optimized to save gas. See this link for an example of how it works. Below are the interfaces/abstract contracts that can be optimized so that the most frequently-called functions use the least amount of gas possible during method lookup. Method IDs that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shifted

There are 3 instances of this issue:

File: src/periphery/LendgineRouter.sol

/// @audit mint(), burn()
20:   contract LendgineRouter is Multicall, Payment, SelfPermit, SwapHelper, IMintCallback, IPairMintCallback {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LendgineRouter.sol#L20

File: src/periphery/LiquidityManager.sol

/// @audit pairMintCallback(), addLiquidity(), removeLiquidity(), collect()
16:   contract LiquidityManager is Multicall, Payment, SelfPermit, IPairMintCallback {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L16

File: src/periphery/Payment.sol

/// @audit unwrapWETH(), sweepToken(), refundETH()
12:   abstract contract Payment {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/Payment.sol#L12

[G‑07] Use a more recent version of solidity

Use a solidity version of at least 0.8.0 to get overflow protection without SafeMath Use a solidity version of at least 0.8.2 to get simple compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value

There are 2 instances of this issue:

File: src/core/libraries/PositionMath.sol

2:    pragma solidity >=0.5.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/libraries/PositionMath.sol#L2

File: src/periphery/libraries/LendgineAddress.sol

2:    pragma solidity >=0.5.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/libraries/LendgineAddress.sol#L2

[G‑08] Use a more recent version of solidity

Use a solidity version of at least 0.8.2 to get simple compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value

There are 4 instances of this issue:

File: src/core/ImmutableState.sol

2:    pragma solidity ^0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/ImmutableState.sol#L2

File: src/core/JumpRate.sol

2:    pragma solidity ^0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/JumpRate.sol#L2

File: src/libraries/SafeCast.sol

2:    pragma solidity ^0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/libraries/SafeCast.sol#L2

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

1:    pragma solidity >=0.8.0;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L1

[G‑09] >= costs less gas than >

The compiler uses opcodes GT and ISZERO for solidity code that uses >, but only requires LT for >=, which saves 3 gas

There are 3 instances of this issue:

File: src/core/Lendgine.sol

198:      collateral = collateralRequested > tokensOwed ? tokensOwed : collateralRequested;

253:      uint256 dilutionLP = dilutionLPRequested > _totalLiquidityBorrowed ? _totalLiquidityBorrowed : dilutionLPRequested;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L198

File: src/periphery/LiquidityManager.sol

241:      amount = params.amountRequested > position.tokensOwed ? position.tokensOwed : params.amountRequested;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L241

[G‑10] Splitting require() statements that use && saves gas

See this issue which describes the fact that there is a larger deployment gas cost, but with enough runtime calls, the change ends up being cheaper by 3 gas

There are 2 instances of this issue:

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

61:       require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");

79:       require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L61

[G‑11] Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead

When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.

https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Each operation involving a uint8 costs an extra 22-28 gas (depending on whether the other operand is also a variable of type uint8) as compared to ones involving uint256, due to the compiler having to clear the higher bits of the memory word before operating on the uint8, as well as the associated stack operations of doing so. Use a larger size then downcast where needed

There are 7 instances of this issue:

File: src/core/Pair.sol

/// @audit uint120 reserve0
85:       reserve0 = _reserve0 + SafeCast.toUint120(amount0In); // SSTORE

/// @audit uint120 reserve1
86:       reserve1 = _reserve1 + SafeCast.toUint120(amount1In); // SSTORE

/// @audit uint120 reserve0
108:      reserve0 = _reserve0 - SafeCast.toUint120(amount0); // SSTORE

/// @audit uint120 reserve1
109:      reserve1 = _reserve1 - SafeCast.toUint120(amount1); // SSTORE

/// @audit uint120 reserve0
135:      reserve0 = _reserve0 + SafeCast.toUint120(amount0In) - SafeCast.toUint120(amount0Out); // SSTORE

/// @audit uint120 reserve1
136:      reserve1 = _reserve1 + SafeCast.toUint120(amount1In) - SafeCast.toUint120(amount1Out); // SSTORE

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Pair.sol#L85

File: src/libraries/SafeCast.sol

/// @audit uint120 z
9:        require((z = uint120(y)) == y);

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/libraries/SafeCast.sol#L9

[G‑12] Stack variable used as a cheaper cache for a state variable is only used once

If the variable is only accessed once, it's cheaper to use the state variable directly that one time, and save the 3 gas the extra stack assignment would spend

There is 1 instance of this issue:

File: src/core/Lendgine.sol

163:      uint256 _totalPositionSize = totalPositionSize; // SLOAD

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Lendgine.sol#L163

[G‑13] Multiple if-statements with mutually-exclusive conditions should be changed to if-else statements

If two conditions are the same, their blocks should be combined

There is 1 instance of this issue:

File: src/core/libraries/Position.sol

55        if (sizeDelta == 0) {
56          if (_positionInfo.size == 0) revert NoPositionError();
57          sizeNext = _positionInfo.size;
58        } else {
59          sizeNext = PositionMath.addDelta(_positionInfo.size, sizeDelta);
60        }
61    
62:       if (sizeDelta != 0) positionInfo.size = sizeNext;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/libraries/Position.sol#L55-L62

[G‑14] Constructors can be marked payable

Payable functions cost less gas to execute, since the compiler does not have to add extra checks to ensure that a payment wasn't provided. A constructor can safely be marked as payable, since only the deployer would be able to pass funds, and the project itself would not pass any funds.

There are 5 instances of this issue:

File: src/core/ImmutableState.sol

27      constructor() {
28:       factory = msg.sender;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/ImmutableState.sol#L27-L28

File: src/periphery/LendgineRouter.sol

49      constructor(
50        address _factory,
51        address _uniswapV2Factory,
52        address _uniswapV3Factory,
53        address _weth
54      )
55        SwapHelper(_uniswapV2Factory, _uniswapV3Factory)
56:       Payment(_weth)

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LendgineRouter.sol#L49-L56

File: src/periphery/LiquidityManager.sol

75:     constructor(address _factory, address _weth) Payment(_weth) {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/LiquidityManager.sol#L75

File: src/periphery/Payment.sol

17:     constructor(address _weth) {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/Payment.sol#L17

File: src/periphery/SwapHelper.sol

29:     constructor(address _uniswapV2Factory, address _uniswapV3Factory) {

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/SwapHelper.sol#L29


Excluded findings

These findings are excluded from awards calculations because there are publicly-available automated tools that find them. The valid ones appear here for completeness

Summary

Gas Optimizations

IssueInstancesTotal Gas Saved
[G‑01]require()/revert() strings longer than 32 bytes cost extra gas5-
[G‑02]Using > 0 costs more gas than != 0 when used on a uint in a require() statement212
[G‑03]Division by two should use bit shifting120
[G‑04]Use custom errors rather than revert()/require() strings to save gas9-

Total: 17 instances over 4 issues with 32 gas saved

Gas totals use lower bounds of ranges and count two iterations of each for-loop. All values above are runtime, not deployment, values; deployment values are listed in the individual issue descriptions. The table above as well as its gas numbers do not include any of the excluded findings.

Gas Optimizations

[G‑01] require()/revert() strings longer than 32 bytes cost extra gas

Each extra memory word of bytes past the original 32 incurs an MSTORE which costs 3 gas

There are 5 instances of this issue:

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

/// @audit (valid but excluded finding)
11:       require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES");

/// @audit (valid but excluded finding)
60:       require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");

/// @audit (valid but excluded finding)
61:       require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");

/// @audit (valid but excluded finding)
78:       require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT");

/// @audit (valid but excluded finding)
79:       require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L11

[G‑02] Using > 0 costs more gas than != 0 when used on a uint in a require() statement

This change saves 6 gas per instance. The optimization works until solidity version 0.8.13 where there is a regression in gas costs.

There are 2 instances of this issue:

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

/// @audit (valid but excluded finding)
60:       require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");

/// @audit (valid but excluded finding)
78:       require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT");

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L60

[G‑03] Division by two should use bit shifting

<x> / 2 is the same as <x> >> 1. While the compiler uses the SHR opcode to accomplish both, the version that uses division incurs an overhead of 20 gas due to JUMPs to and from a compiler utility function that introduces checks which can be avoided by using unchecked {} around the division by two

There is 1 instance of this issue:

File: src/core/Pair.sol

/// @audit (valid but excluded finding)
63:       uint256 c = (scale1 * scale1) / 4;

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/Pair.sol#L63

[G‑04] Use custom errors rather than revert()/require() strings to save gas

Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hit by avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas

There are 9 instances of this issue:

File: src/core/libraries/PositionMath.sol

/// @audit (valid but excluded finding)
14:         require((z = x - uint256(-y)) < x, "LS");

/// @audit (valid but excluded finding)
16:         require((z = x + uint256(y)) >= x, "LA");

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/core/libraries/PositionMath.sol#L14

File: src/periphery/Payment.sol

/// @audit (valid but excluded finding)
22:       require(msg.sender == weth, "Not WETH9");

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/Payment.sol#L22

File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

/// @audit (valid but excluded finding)
11:       require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES");

/// @audit (valid but excluded finding)
13:       require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS");

/// @audit (valid but excluded finding)
60:       require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");

/// @audit (valid but excluded finding)
61:       require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");

/// @audit (valid but excluded finding)
78:       require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT");

/// @audit (valid but excluded finding)
79:       require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");

https://github.com/code-423n4/2023-01-numoen/blob/c36e531dcadaec7dee8c61332f44870e4fafca13/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L11

#0 - c4-judge

2023-02-08T15:22:56Z

berndartmueller marked the issue as grade-a

#1 - c4-judge

2023-02-08T15:25:07Z

berndartmueller marked the issue as selected for report

#2 - noamyakov

2023-02-18T23:12:43Z

Some of the instances described in this gas report are false and the report saves less gas than claimed.

[G‑02] Using storage instead of memory for structs/arrays saves gas

File: src/core/libraries/Position.sol

47:       Position.Info memory _positionInfo = positionInfo;

All of _positionInfo's fields are read, so no gas will be saved. size is read on line 50, rewardPerPositionPaid is read on line 70 (inside newTokensOwed()) and tokensOwed is read on line 64.

File: src/periphery/LiquidityManager.sol

211:      Position memory position = positions[msg.sender][lendgine]; // SLOAD

All of position's fields are read, so no gas will be saved. size is read on line 214, rewardPerPositionPaid is read on line 214 and tokensOwed is read on line 214.

Total gas actually saved: 4200

[G‑03] Avoid contract existence checks by using low level calls

File: src/core/Pair.sol

/// @audit swapCallback()
127:      ISwapCallback(msg.sender).swapCallback(amount0Out, amount1Out, data);

swapCallback() doesn't have a return value so the optimization doesn't apply here and no gas will be saved.

File: src/periphery/Payment.sol

/// @audit withdraw()
30:         IWETH9(weth).withdraw(balanceWETH);

withdraw() doesn't have a return value so the optimization doesn't apply here and no gas will be saved.

Total gas actually saved: 1800


Overall, this gas report actually saves a total of 6730 gas. Therefore, I think report #98 is better and should be selected for the report.

#3 - berndartmueller

2023-02-23T10:52:18Z

I invite @IllIllI000 to add feedback here.

#4 - IllIllI000

2023-02-23T13:58:20Z

noamyakov is right about the second G-02, and both of the G-03s (the G-03 bugs I've fixed). I haven't looked at his report so I can't vouch for correctness, but if his numbers are right, then yes, he should be selected

#5 - berndartmueller

2023-02-23T16:03:31Z

I will reconsider my decision to select this gas submission for the report. Thanks @noamyakov, for pointing out the inaccuracies, and big thanks to @IllIllI000 for being such a collegial fellow warden.

G-02: No possible gas savings in both mentioned cases by Noam

G-03: Due to safety precautions, the EXTCODESIZE check done by Solidity is valid in the case of swapCallback() and withdraw to ensure a contract exists at the target address. Hence, the gas optimization recommendations are invalid.

#6 - c4-judge

2023-02-23T16:03:40Z

berndartmueller marked the issue as not selected for report

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Ā© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter