Numoen contest - 0xSmartContract'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: 10/31

Findings: 2

Award: $1,042.54

QA:
grade-a
Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Labels

bug
grade-a
QA (Quality Assurance)
edited-by-warden
Q-06

Awards

997.1116 USDC - $997.11

External Links

Summary

Low Risk Issues List

NumberIssues DetailsContext
[L-01]Some tokens can have 2 addresses, so should be done check other require1
[L-02]createLendgine event is missing parameters1
[L-03]Missing Event for initialize5
[L-04]Omissions in Events1
[L-05]Lack of control to assign 0 values in the value assignments of critical state variables in the constructor4
[L-06]Add to Event-Emit for critical function1
[L-07]Solmate's SafeTransferLib doesn't check whether the ERC20 contract exists29
[L-08]Loss of precision due to rounding1
[L-09]Cross-chain replay attacks are possible with  computeAddress 1
[L-10]Use Fuzzing Test for complicated dex code basesAll Contracts
[L-11]Using >/>= without specifying an upper bound is unsafe3

Total 11 issues

Non-Critical Issues List

NumberIssues DetailsContext
[NC-01]Insufficient coverage1
[NC-02]NatSpec comments should be increased in contractsAll Contracts
[NC-03]Function writing that does not comply with the Solidity Style GuideAll Contracts
[NC-04]Include return parameters in NatSpec commentsAll Contracts
[NC-05]Tokens accidentally sent to the contract cannot be recovered
[NC-06]Take advantage of Custom Error's return value property37
[NC-07]Repeated validation logic can be converted into a function modifier4
[NC-08]Use a more recent version of SolidityAll contracts
[NC-09]For functions, follow Solidity standard naming conventions (internal function style rule)12
[NC-10]Floating pragma12
[NC-11]Project Upgrade and Stop Scenario should be
[NC-12]Use descriptive names for Contracts and Libraries
[NC-13]Use SMTChecker
[NC-14]Add NatSpec Mapping comment3
[NC-15]Require messages are too short or not6
[NC-16]Use underscores for number literals2
[NC-17]Showing the actual values of numbers in NatSpec comments makes checking and reading code easier1
[NC-18]Lines are too long12

Total 18 issues

[L-01] Some tokens can have 2 addresses, so should be done check other require

Impact

In the Factory.createLendgine() function, the Pair consisting of address token0 and token1 is checked for uniqueness;


src/core/Factory.sol:
  62    /// @inheritdoc IFactory
  63:   function createLendgine(
  64:     address token0,
  65:     address token1,
  66:     uint8 token0Exp,
  67:     uint8 token1Exp,
  68:     uint256 upperBound
  69:   )
  70:     external
  71:     override
  72:     returns (address lendgine)
  73:   {
  74:     if (token0 == token1) revert SameTokenError();
  75:     if (token0 == address(0) || token1 == address(0)) revert ZeroAddressError();
  76:     if (getLendgine[token0][token1][token0Exp][token1Exp][upperBound] != address(0)) revert DeployedError();
  77:     if (token0Exp > 18 || token0Exp < 6 || token1Exp > 18 || token1Exp < 6) revert ScaleError();
  78: 
  79:     parameters =
  80:       Parameters({token0: token0, token1: token1, token0Exp: token0Exp, token1Exp: token1Exp, upperBound: upperBound});
  81: 
  82:     lendgine = address(new Lendgine{ salt: keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)) }());
  83: 
  84:     delete parameters;
  85: 
  86:     getLendgine[token0][token1][token0Exp][token1Exp][upperBound] = lendgine;
  87:     emit LendgineCreated(token0, token1, token0Exp, token1Exp, upperBound, lendgine);
  88:   }
  89: }

But some tokens have 2 addresses through which they can be interacted with.

https://medium.com/chainsecurity/trueusd-compound-vulnerability-bc5b696d29e2

One of the two addresses can be used when interacting with the relevant token, so a check should be added additionally to prevent tokens that can be interacted with address 2

The name and symbol of Token0 and Token1 can be stored in a mapping and a require can be added to check with every existing Token0 and Token1

[L-02] createLendgine event is missing parameters

The Factory.sol contract has very important function; createLendgine

However, only amounts are published in emits, whereas msg.sender must be specified in every transaction, a contract or web page listening to events cannot react to users, emit does not serve the purpose. Basically, this event cannot be used

src/core/Factory.sol:
  62    /// @inheritdoc IFactory
  63:   function createLendgine(
  64:     address token0,
  65:     address token1,
  66:     uint8 token0Exp,
  67:     uint8 token1Exp,
  68:     uint256 upperBound
  69:   )
  70:     external
  71:     override
  72:     returns (address lendgine)
  73:   {
  74:     if (token0 == token1) revert SameTokenError();
  75:     if (token0 == address(0) || token1 == address(0)) revert ZeroAddressError();
  76:     if (getLendgine[token0][token1][token0Exp][token1Exp][upperBound] != address(0)) revert DeployedError();
  77:     if (token0Exp > 18 || token0Exp < 6 || token1Exp > 18 || token1Exp < 6) revert ScaleError();
  78: 
  79:     parameters =
  80:       Parameters({token0: token0, token1: token1, token0Exp: token0Exp, token1Exp: token1Exp, upperBound: upperBound});
  81: 
  82:     lendgine = address(new Lendgine{ salt: keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)) }());
  83: 
  84:     delete parameters;
  85: 
  86:     getLendgine[token0][token1][token0Exp][token1Exp][upperBound] = lendgine;
  87:     emit LendgineCreated(token0, token1, token0Exp, token1Exp, upperBound, lendgine);
  88:   }
  89: }

add msg.sender parameter in event-emit

[L-03] Missing Event for initialize

Context:


5 result
src/core/ImmutableState.sol:
  26  
  27:   constructor() {
  28:     factory = msg.sender;
  29: 
  30:     uint128 _token0Exp;
  31:     uint128 _token1Exp;
  32: 
  33:     (token0, token1, _token0Exp, _token1Exp, upperBound) = Factory(msg.sender).parameters();
  34: 
  35:     token0Scale = 10 ** (18 - _token0Exp);
  36:     token1Scale = 10 ** (18 - _token1Exp);
  37:   }
  38: }


src/periphery/LendgineRouter.sol:
  48  
  49:   constructor(
  50:     address _factory,
  51:     address _uniswapV2Factory,
  52:     address _uniswapV3Factory,
  53:     address _weth
  54:   )
  55:     SwapHelper(_uniswapV2Factory, _uniswapV3Factory)
  56:     Payment(_weth)
  57:   {
  58:     factory = _factory;
  59:   }


src/periphery/LiquidityManager.sol:
  74  
  75:   constructor(address _factory, address _weth) Payment(_weth) {
  76:     factory = _factory;
  77:   }

src/periphery/Payment.sol:
  16  
  17:   constructor(address _weth) {
  18:     weth = _weth;
  19:   }

src/periphery/SwapHelper.sol:
  29:   constructor(address _uniswapV2Factory, address _uniswapV3Factory) {
  30:     uniswapV2Factory = _uniswapV2Factory;
  31:     uniswapV3Factory = _uniswapV3Factory;
  32:   }

Description: Events help non-contract tools to track changes, and events prevent users from being surprised by changes Issuing event-emit during initialization is a detail that many projects skip

Recommendation: Add Event-Emit

[L-04] Omissions in Events

Throughout the codebase, events are generally emitted when sensitive changes are made to the contracts. However, some events are missing important parameters

The events should include the new value and old value where possible:


src/core/Lendgine.sol:
  265    /// @param owner The address that this position belongs to
  266:   function _accruePositionInterest(address owner) private {
  267:     uint256 _rewardPerPositionStored = rewardPerPositionStored; // SLOAD
  268: 
  269:     positions.update(owner, 0, _rewardPerPositionStored);
  270: 
+             OldOwner = self[owner];
- 271:     emit AccruePositionInterest(owner, _rewardPerPositionStored);
+ 271:     emit AccruePositionInterest(OldOwner, owner,  _rewardPerPositionStored);

  272:   }

[L-05] Lack of control to assign 0 values in the value assignments of critical state variables in the constructor

Critical values should be check 0 value in constructor If values set the 0 by mistake, even for a while, it will cause a loss to the project.


4 result

src/periphery/LendgineRouter.sol:
  48  
  49:   constructor(
  50:     address _factory,
  51:     address _uniswapV2Factory,
  52:     address _uniswapV3Factory,
  53:     address _weth
  54:   )
  55:     SwapHelper(_uniswapV2Factory, _uniswapV3Factory)
  56:     Payment(_weth)
  57:   {
  58:     factory = _factory;
  59:   }


src/periphery/LiquidityManager.sol:
  74  
  75:   constructor(address _factory, address _weth) Payment(_weth) {
  76:     factory = _factory;
  77:   }

src/periphery/Payment.sol:
  16  
  17:   constructor(address _weth) {
  18:     weth = _weth;
  19:   }

src/periphery/SwapHelper.sol:
  29:   constructor(address _uniswapV2Factory, address _uniswapV3Factory) {
  30:     uniswapV2Factory = _uniswapV2Factory;
  31:     uniswapV3Factory = _uniswapV3Factory;
  32:   }

[L-06] Add to Event-Emit for critical function


src/periphery/LendgineRouter.sol:
   86    /// @notice Transfer the necessary amount of token1 to mint an option position
   87:   function mintCallback(
   88:     uint256 collateralTotal,
   89:     uint256 amount0,
   90:     uint256 amount1,
   91:     uint256,
   92:     bytes calldata data
   93:   )
   94:     external
   95:     override
   96:   {
   97:     MintCallbackData memory decoded = abi.decode(data, (MintCallbackData));
   98: 
   99:     address lendgine = LendgineAddress.computeAddress(
  100:       factory, decoded.token0, decoded.token1, decoded.token0Exp, decoded.token1Exp, decoded.upperBound
  101:     );
  102:     if (lendgine != msg.sender) revert ValidationError();
  103: 
  104:     // swap all token0 to token1
  105:     uint256 collateralSwap = swap(
  106:       decoded.swapType,
  107:       SwapParams({
  108:         tokenIn: decoded.token0,
  109:         tokenOut: decoded.token1,
  110:         amount: SafeCast.toInt256(amount0),
  111:         recipient: msg.sender
  112:       }),
  113:       decoded.swapExtraData
  114:     );
  115: 
  116:     // send token1 back
  117:     SafeTransferLib.safeTransfer(decoded.token1, msg.sender, amount1);
  118: 
  119:     // pull the rest of tokens from the user
  120:     uint256 collateralIn = collateralTotal - amount1 - collateralSwap;
  121:     if (collateralIn > decoded.collateralMax) revert AmountError();
  122: 
  123:     pay(decoded.token1, decoded.payer, msg.sender, collateralIn);
  124:   }

[L-07] Solmate's SafeTransferLib doesn't check whether the ERC20 contract exists

Solmate's SafeTransferLib, which is often used to interact with non-compliant/unsafe ERC20 tokens, does not check whether the ERC20 contract exists. The following code will not revert in case the token doesn't exist (yet).

This is stated in the Solmate library: https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol#L9

29 results 

src/core/Lendgine.sol:
   13  import { Position } from "./libraries/Position.sol";
   14: import { SafeTransferLib } from "../libraries/SafeTransferLib.sol";
   15  import { SafeCast } from "../libraries/SafeCast.sol";

  115      _burn(address(this), shares);
  116:     SafeTransferLib.safeTransfer(token1, to, collateral); // optimistically transfer
  117      mint(liquidity, data);

  201        position.tokensOwed = tokensOwed - collateral; // SSTORE
  202:       SafeTransferLib.safeTransfer(token1, to, collateral);
  203      }

src/core/Pair.sol:
   13  import { SafeCast } from "../libraries/SafeCast.sol";
   14: import { SafeTransferLib } from "../libraries/SafeTransferLib.sol";
   15  

  101  
  102:     if (amount0 > 0) SafeTransferLib.safeTransfer(token0, to, amount0);
  103:     if (amount1 > 0) SafeTransferLib.safeTransfer(token1, to, amount1);
  104  

  121  
  122:     if (amount0Out > 0) SafeTransferLib.safeTransfer(token0, to, amount0Out);
  123:     if (amount1Out > 0) SafeTransferLib.safeTransfer(token1, to, amount1Out);
  124  

src/periphery/LendgineRouter.sol:
   15  import { SafeCast } from "../libraries/SafeCast.sol";
   16: import { SafeTransferLib } from "../libraries/SafeTransferLib.sol";
   17  

  116      // send token1 back
  117:     SafeTransferLib.safeTransfer(decoded.token1, msg.sender, amount1);
  118  

  165  
  166:     SafeTransferLib.safeTransfer(lendgine, params.recipient, shares);
  167  

  227      // pay token1
  228:     SafeTransferLib.safeTransfer(decoded.token1, msg.sender, amount1);
  229  

  235      if (decoded.recipient != address(this)) {
  236:       SafeTransferLib.safeTransfer(decoded.token1, decoded.recipient, collateralOut);
  237      }

  263  
  264:     SafeTransferLib.safeTransferFrom(lendgine, msg.sender, lendgine, params.shares);
  265  

src/periphery/Payment.sol:
   6  import { Balance } from "./../libraries/Balance.sol";
   7: import { SafeTransferLib } from "./../libraries/SafeTransferLib.sol";
   8  

  30        IWETH9(weth).withdraw(balanceWETH);
  31:       SafeTransferLib.safeTransferETH(recipient, balanceWETH);
  32      }

  39      if (balanceToken > 0) {
  40:       SafeTransferLib.safeTransfer(token, recipient, balanceToken);
  41      }

  44    function refundETH() external payable {
  45:     if (address(this).balance > 0) SafeTransferLib.safeTransferETH(msg.sender, address(this).balance);
  46    }

  55        IWETH9(weth).deposit{value: value}(); // wrap only what is needed to pay
  56:       SafeTransferLib.safeTransfer(weth, recipient, value);
  57      } else if (payer == address(this)) {
  58        // pay with tokens already in the contract (for the exact input multihop case)
  59:       SafeTransferLib.safeTransfer(token, recipient, value);
  60      } else {
  61        // pull payment
  62:       SafeTransferLib.safeTransferFrom(token, payer, recipient, value);
  63      }

src/periphery/SwapHelper.sol:
   8  import { PoolAddress } from "./UniswapV3/libraries/PoolAddress.sol";
   9: import { SafeTransferLib } from "../libraries/SafeTransferLib.sol";
  10  import { TickMath } from "./UniswapV3/libraries/TickMath.sol";

  41  
  42:     SafeTransferLib.safeTransfer(tokenIn, msg.sender, amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta));
  43    }

  86  
  87:       SafeTransferLib.safeTransfer(params.tokenIn, pair, amountIn);
  88        IUniswapV2Pair(pair).swap(amount0Out, amount1Out, params.recipient, bytes(""));

Add a contract exist control in functions;

pragma solidity >=0.8.0;

function isContract(address _addr) private returns (bool isContract) {
    isContract = _addr.code.length > 0;
}

[L-08] Loss of precision due to rounding

Add scalar so roundings are negligible

src/periphery/UniswapV2/libraries/UniswapV2Library.sol:
 65:     amountOut = numerator / denominator;

src/periphery/UniswapV2/libraries/UniswapV2Library.sol:
  50    // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
  51:   function getAmountOut(
  52:     uint256 amountIn,
  53:     uint256 reserveIn,
  54:     uint256 reserveOut
  55:   )
  56:     internal
  57:     pure
  58:     returns (uint256 amountOut)
  59:   {
  60:     require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");
  61:     require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
  62:     uint256 amountInWithFee = amountIn * 997;
  63:     uint256 numerator = amountInWithFee * reserveOut;
  64:     uint256 denominator = (reserveIn * 1000) + amountInWithFee;
  65:     amountOut = numerator / denominator;
  66:   }

[L-09] Cross-chain replay attacks are possible with  computeAddress

The computeAddress() function can generate the same address on different networks if the network is forked (Ethereum network has been forked in the past and may be forked in the future), funds may be compromised

https://mirror.xyz/0xbuidlerdao.eth/lOE5VN-BHI0olGOXe27F0auviIuoSlnou_9t3XRJseY

There is no chain.id in the keccak256data

src/periphery/libraries/LendgineAddress.sol:
   8  
   9:   function computeAddress(
  10:     address factory,
  11:     address token0,
  12:     address token1,
  13:     uint256 token0Exp,
  14:     uint256 token1Exp,
  15:     uint256 upperBound
  16:   )
  17:     internal
  18:     pure
  19:     returns (address lendgine)
  20:   {
  21:     lendgine = address(
  22:       uint160(
  23:         uint256(
  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:           )
  32:         )
  33:       )
  34:     );
  35:   }
  36  }


Similar issue from Harpieio; https://github.com/Harpieio/contracts/pull/4/commits/de24a50349ec014163180ba60b5305098f42eb14

Include the chain.id in keccak256

[L-10] Use Fuzzing Test for complicated dex code bases

20 results - 5 files

src/core/Lendgine.sol:
  214      uint256 _totalLiquidityBorrowed = totalLiquidityBorrowed; // SLOAD
  215:     return _totalLiquidityBorrowed == 0 ? liquidity : FullMath.mulDiv(liquidity, totalSupply, _totalLiquidityBorrowed);
  216    }

  219    function convertShareToLiquidity(uint256 shares) public view override returns (uint256) {
  220:     return FullMath.mulDiv(totalLiquidityBorrowed, shares, totalSupply);
  221    }

  224    function convertCollateralToLiquidity(uint256 collateral) public view override returns (uint256) {
  225:     return FullMath.mulDiv(collateral * token1Scale, 1e18, 2 * upperBound);
  226    }

  229    function convertLiquidityToCollateral(uint256 liquidity) public view override returns (uint256) {
  230:     return FullMath.mulDiv(liquidity, 2 * upperBound, 1e18) / token1Scale;
  231    }

  251  
  252:     uint256 dilutionLPRequested = (FullMath.mulDiv(borrowRate, _totalLiquidityBorrowed, 1e18) * timeElapsed) / 365 days;
  253      uint256 dilutionLP = dilutionLPRequested > _totalLiquidityBorrowed ? _totalLiquidityBorrowed : dilutionLPRequested;

  256      totalLiquidityBorrowed = _totalLiquidityBorrowed - dilutionLP;
  257:     rewardPerPositionStored += FullMath.mulDiv(dilutionSpeculative, 1e18, totalPositionSize);
  258      lastUpdate = block.timestamp;

src/core/Pair.sol:
  55  
  56:     uint256 scale0 = FullMath.mulDiv(amount0, 1e18, liquidity) * token0Scale;
  57:     uint256 scale1 = FullMath.mulDiv(amount1, 1e18, liquidity) * token1Scale;
  58  

  97  
  98:     amount0 = FullMath.mulDiv(_reserve0, liquidity, _totalLiquidity);
  99:     amount1 = FullMath.mulDiv(_reserve1, liquidity, _totalLiquidity);
  100      if (amount0 == 0 && amount1 == 0) revert InsufficientOutputError();

src/core/libraries/Position.sol:
  69    function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) {
  70:     return FullMath.mulDiv(position.size, rewardPerPosition - position.rewardPerPositionPaid, 1 ether);
  71    }

  82      return
  83:       totalLiquiditySupplied == 0 ? liquidity : FullMath.mulDiv(liquidity, totalPositionSize, totalLiquiditySupplied);
  84    }

  94    {
  95:     return FullMath.mulDiv(position, totalLiquiditySupplied, totalPositionSize);
  96    }

src/periphery/LendgineRouter.sol:
  208      } else {
  209:       amount0 = FullMath.mulDivRoundingUp(liquidity, r0, totalLiquidity);
  210:       amount1 = FullMath.mulDivRoundingUp(liquidity, r1, totalLiquidity);
  211      }

src/periphery/LiquidityManager.sol:
  150      } else {
  151:       amount0 = FullMath.mulDivRoundingUp(params.liquidity, r0, totalLiquidity);
  152:       amount1 = FullMath.mulDivRoundingUp(params.liquidity, r1, totalLiquidity);
  153      }

  177      (, uint256 rewardPerPositionPaid,) = ILendgine(lendgine).positions(address(this));
  178:     position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18);
  179      position.rewardPerPositionPaid = rewardPerPositionPaid;

  213      (, uint256 rewardPerPositionPaid,) = ILendgine(lendgine).positions(address(this));
  214:     position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18);
  215      position.rewardPerPositionPaid = rewardPerPositionPaid;

  237      (, uint256 rewardPerPositionPaid,) = ILendgine(params.lendgine).positions(address(this));
  238:     position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18);
  239      position.rewardPerPositionPaid = rewardPerPositionPaid;

Description:

I recommend fuzzing testing in complex code structures like Dex conventions, especially Numoen, where there is an innovative code base and a lot of computation.

Recommendation:

Use should fuzzing test like Echidna.

As Alberto Cuesta Canada said:

Fuzzing is not easy, the tools are rough, and the math is hard, but it is worth it. Fuzzing gives me a level of confidence in my smart contracts that I didn’t have before. Relying just on unit testing anymore and poking around in a testnet seems reckless now.

https://medium.com/coinmonks/smart-contract-fuzzing-d9b88e0b0a05

[L-11] Using >/>= without specifying an upper bound is unsafe

3 results - 3 files

src/core/libraries/PositionMath.sol:
  2: pragma solidity >=0.5.0;

src/periphery/libraries/LendgineAddress.sol:
  2: pragma solidity >=0.5.0;

src/periphery/UniswapV2/libraries/UniswapV2Library.sol:
  1: pragma solidity >=0.8.0;

[N-01] Insufficient coverage

Description: The test coverage rate of the project is ~60%. Testing all functions is best practice in terms of security criteria. Due to its capacity, test coverage is expected to be 100%.

| File                                                   | % Lines          | % Statements     | % Branches      | % Funcs        |
|--------------------------------------------------------|------------------|------------------|-----------------|----------------|
| src/core/ERC20.sol                                     | 70.37% (19/27)   | 66.67% (20/30)   | 33.33% (2/6)    | 62.50% (5/8)   |
| src/core/Factory.sol                                   | 100.00% (9/9)    | 92.31% (12/13)   | 87.50% (7/8)    | 100.00% (1/1)  |
| src/core/JumpRate.sol                                  | 72.73% (8/11)    | 64.71% (11/17)   | 75.00% (3/4)    | 66.67% (2/3)   |
| src/core/Lendgine.sol                                  | 100.00% (80/80)  | 99.04% (103/104) | 96.15% (25/26)  | 84.62% (11/13) |
| src/core/Pair.sol                                      | 100.00% (51/51)  | 93.24% (69/74)   | 77.27% (17/22)  | 100.00% (4/4)  |
| src/core/libraries/Position.sol                        | 0.00% (0/16)     | 0.00% (0/19)     | 0.00% (0/10)    | 0.00% (0/4)    |
| src/core/libraries/PositionMath.sol                    | 0.00% (0/3)      | 0.00% (0/3)      | 0.00% (0/6)     | 0.00% (0/1)    |
| src/libraries/Balance.sol                              | 100.00% (4/4)    | 80.00% (4/5)     | 50.00% (1/2)    | 100.00% (1/1)  |
| src/libraries/FullMath.sol                             | 0.00% (0/30)     | 0.00% (0/32)     | 0.00% (0/8)     | 0.00% (0/2)    |
| src/libraries/SafeCast.sol                             | 0.00% (0/3)      | 0.00% (0/3)      | 0.00% (0/4)     | 0.00% (0/2)    |
| src/libraries/SafeTransferLib.sol                      | 40.00% (4/10)    | 25.00% (3/12)    | 14.29% (1/7)    | 50.00% (2/4)   |
| src/periphery/LendgineRouter.sol                       | 0.00% (0/39)     | 0.00% (0/60)     | 0.00% (0/16)    | 0.00% (0/4)    |
| src/periphery/LiquidityManager.sol                     | 6.12% (3/49)     | 8.45% (6/71)     | 6.25% (1/16)    | 50.00% (2/4)   |
| src/periphery/Multicall.sol                            | 0.00% (0/6)      | 0.00% (0/10)     | 0.00% (0/4)     | 0.00% (0/1)    |
| src/periphery/Payment.sol                              | 0.00% (0/16)     | 0.00% (0/21)     | 0.00% (0/14)    | 0.00% (0/4)    |
| src/periphery/SelfPermit.sol                           | 0.00% (0/2)      | 0.00% (0/2)      | 100.00% (0/0)   | 0.00% (0/2)    |
| src/periphery/SwapHelper.sol                           | 0.00% (0/23)     | 0.00% (0/30)     | 0.00% (0/6)     | 0.00% (0/2)    |
| src/periphery/UniswapV2/libraries/UniswapV2Library.sol | 0.00% (0/19)     | 0.00% (0/27)     | 0.00% (0/12)    | 0.00% (0/5)    |
| src/periphery/UniswapV3/libraries/PoolAddress.sol      | 0.00% (0/4)      | 0.00% (0/5)      | 0.00% (0/4)     | 0.00% (0/2)    |
| src/periphery/libraries/LendgineAddress.sol            | 100.00% (1/1)    | 100.00% (1/1)    | 100.00% (0/0)   | 100.00% (1/1)  |
| Total                                                  | 39.56% (180/455) | 38.40% (230/599) | 28.08% (57/203) | 37.97% (30/79) |

[N-02] NatSpec comments should be increased in contracts

Context: All Contracts

Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability. https://docs.soliditylang.org/en/v0.8.15/natspec-format.html

Recommendation: NatSpec comments should be increased in contracts

[N-03] Function writing that does not comply with the Solidity Style Guide

Context: All Contracts

Description: Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.

https://docs.soliditylang.org/en/v0.8.17/style-guide.html

Functions should be grouped according to their visibility and ordered:

constructor receive function (if exists) fallback function (if exists) external public internal private within a grouping, place the view and pure functions last

[N-04] Include return parameters in NatSpec comments

Context: All Contracts

Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.

https://docs.soliditylang.org/en/v0.8.15/natspec-format.html

Recommendation: Include return parameters in NatSpec comments

Recommendation Code Style: (from Uniswap3)

    /// @notice Adds liquidity for the given recipient/tickLower/tickUpper position
    /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback
    /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends
    /// on tickLower, tickUpper, the amount of liquidity, and the current price.
    /// @param recipient The address for which the liquidity will be created
    /// @param tickLower The lower tick of the position in which to add liquidity
    /// @param tickUpper The upper tick of the position in which to add liquidity
    /// @param amount The amount of liquidity to mint
    /// @param data Any data that should be passed through to the callback
    /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback
    /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback
    function mint(
        address recipient,
        int24 tickLower,
        int24 tickUpper,
        uint128 amount,
        bytes calldata data
    ) external returns (uint256 amount0, uint256 amount1);

[N-05] Tokens accidentally sent to the contract cannot be recovered

It can't be recovered if the tokens accidentally arrive at the contract address, which has happened to many popular projects, so I recommend adding a recovery code to your critical contracts.

Add this code:

 /**
  * @notice Sends ERC20 tokens trapped in contract to external address
  * @dev Onlyowner is allowed to make this function call
  * @param account is the receiving address
  * @param externalToken is the token being sent
  * @param amount is the quantity being sent
  * @return boolean value indicating whether the operation succeeded.
  *
 */
  function rescueERC20(address account, address externalToken, uint256 amount) public onlyOwner returns (bool) {
    IERC20(externalToken).transfer(account, amount);
    return true;
  }
}

[N-06] Take advantage of Custom Error's return value property

An important feature of Custom Error is that values such as address, tokenID, msg.value can be written inside the () sign, this kind of approach provides a serious advantage in debugging and examining the revert details of dapps such as tenderly.

37 results - 8 files

src/core/Factory.sol:
  74:     if (token0 == token1) revert SameTokenError();
  75:     if (token0 == address(0) || token1 == address(0)) revert ZeroAddressError();
  76:     if (getLendgine[token0][token1][token0Exp][token1Exp][upperBound] != address(0)) revert DeployedError();
  77:     if (token0Exp > 18 || token0Exp < 6 || token1Exp > 18 || token1Exp < 6) revert ScaleError();

src/core/Lendgine.sol:
   86:     if (collateral == 0 || liquidity == 0 || shares == 0) revert InputError();
   87:     if (liquidity > totalLiquidity) revert CompleteUtilizationError();
   89:     if (totalSupply > 0 && totalLiquidityBorrowed == 0) revert CompleteUtilizationError();
   99:     if (balanceAfter < balanceBefore + collateral) revert InsufficientInputError();
  112:     if (collateral == 0 || liquidity == 0 || shares == 0) revert InputError();
  140:     if (liquidity == 0 || size == 0) revert InputError();
  142:     if (totalLiquiditySupplied == 0 && totalPositionSize > 0) revert CompleteUtilizationError();
  170:     if (liquidity == 0 || size == 0) revert InputError();
  172:     if (size > positionInfo.size) revert InsufficientPositionError();
  173:     if (liquidity > _totalLiquidity) revert CompleteUtilizationError();

src/core/Pair.sol:
   59:     if (scale1 > 2 * upperBound) revert InvariantError();
   82:       revert InvariantError();
  100:     if (amount0 == 0 && amount1 == 0) revert InsufficientOutputError();
  106:     if (!invariant(_reserve0 - amount0, _reserve1 - amount1, _totalLiquidity - liquidity)) revert InvariantError();
  117:     if (amount0Out == 0 && amount1Out == 0) revert InsufficientOutputError();
  132:       revert InvariantError();

src/core/libraries/Position.sol:
  56:       if (_positionInfo.size == 0) revert NoPositionError();

src/libraries/Balance.sol:
  15:     if (!success || data.length < 32) revert BalanceReturnError();

src/periphery/LendgineRouter.sol:
   66:     if (deadline < block.timestamp) revert LivelinessError();
  102:     if (lendgine != msg.sender) revert ValidationError();
  121:     if (collateralIn > decoded.collateralMax) revert AmountError();
  164:     if (shares < params.sharesMin) revert AmountError();
  196:     if (lendgine != msg.sender) revert ValidationError();
  213:     if (amount0 < decoded.amount0Min || amount1 < decoded.amount1Min) revert AmountError();
  233:     if (collateralOut < decoded.collateralMin) revert AmountError();

src/periphery/LiquidityManager.sol:
   84:     if (deadline < block.timestamp) revert LivelinessError();
  110:     if (lendgine != msg.sender) revert ValidationError();
  155:     if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert AmountError();
  173:     if (size < params.sizeMin) revert AmountError();
  209:     if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert AmountError();
  247:     if (collectAmount != amount) revert CollectError(); // extra check for safety

src/periphery/Payment.sol:
  27:     if (balanceWETH < amountMinimum) revert InsufficientOutputError();
  37:     if (balanceToken < amountMinimum) revert InsufficientOutputError();

[N-07] Repeated validation logic can be converted into a function modifier

If a query or logic is repeated over many lines, using a modifier improves the readability and reusability of the code


4 results

src/core/Factory.sol:
  75:     if (token0 == address(0) || token1 == address(0)) revert ZeroAddressError();

src/periphery/LendgineRouter.sol:
  262:     address recipient = params.recipient == address(0) ? address(this) : params.recipient;

src/periphery/LiquidityManager.sol:
  206:     address recipient = params.recipient == address(0) ? address(this) : params.recipient;
  233:     address recipient = params.recipient == address(0) ? address(this) : params.recipient;

[N-08] Use a more recent version of Solidity

Context: All contracts

Description: For security, it is best practice to use the latest Solidity version. For the security fix list in the versions; https://github.com/ethereum/solidity/blob/develop/Changelog.md

Recommendation: Old version of Solidity is used (0.8.0 - 0.8.4), newer version can be used (0.8.17)

[N-09] For functions, follow Solidity standard naming conventions (internal function style rule)

Context:

12 results - 8 files

src/core/Pair.sol:
  70:   function mint(uint256 liquidity, bytes calldata data) internal {
  93:   function burn(address to, uint256 liquidity) internal returns (uint256 amount0, uint256 amount1) {

src/core/libraries/Position.sol:
  38:   function update(mapping(address => Position.Info) storage self,address owner,int256 sizeDelta,uint256 rewardPerPosition) internal
  63:   function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) {

src/core/libraries/PositionMath.sol:
  12:   function addDelta(uint256 x, int256 y) internal pure returns (uint256 z) {

src/libraries/Balance.sol:
  12:   function balance(address token) internal view returns (uint256) {

src/libraries/SafeCast.sol:
   8:   function toUint120(uint256 y) internal pure returns (uint120 z) {
  15:   function toInt256(uint256 y) internal pure returns (int256 z) {

src/periphery/Payment.sol:
  52:   function pay(address token, address payer, address recipient, uint256 value) internal {

src/periphery/SwapHelper.sol:
  69:   function swap(SwapType swapType, SwapParams memory params, bytes memory data) internal returns (uint256 amount) {

src/periphery/UniswapV2/libraries/UniswapV2Library.sol:
  10:   function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
  17:   function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {

Description: The above codes don't follow Solidity's standard naming convention,

internal and private functions : the mixedCase format starting with an underscore (_mixedCase starting with an underscore)

[N-10] Floating pragma

Description: Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively. https://swcregistry.io/docs/SWC-103

Floating Pragma List:

12 results

src/core/Factory.sol:
  2: pragma solidity ^0.8.4;

src/core/ImmutableState.sol:
  2: pragma solidity ^0.8.0;

src/core/JumpRate.sol:
  2: pragma solidity ^0.8.0;

src/core/Lendgine.sol:
  2: pragma solidity ^0.8.4;

src/core/Pair.sol:
  2: pragma solidity ^0.8.4;

src/core/libraries/Position.sol:
  2: pragma solidity ^0.8.4;

src/libraries/Balance.sol:
  2: pragma solidity ^0.8.4;

src/libraries/SafeCast.sol:
  2: pragma solidity ^0.8.0;

src/periphery/LendgineRouter.sol:
  2: pragma solidity ^0.8.4;

src/periphery/LiquidityManager.sol:
  2: pragma solidity ^0.8.4;

src/periphery/Payment.sol:
  2: pragma solidity ^0.8.4;

src/periphery/SwapHelper.sol:
  2: pragma solidity ^0.8.4;

Recommendation: Lock the pragma version and also consider known bugs (https://github.com/ethereum/solidity/releases) for the compiler version that is chosen.

[N-11] Project Upgrade and Stop Scenario should be

At the start of the project, the system may need to be stopped or upgraded, I suggest you have a script beforehand and add it to the documentation. This can also be called an " EMERGENCY STOP (CIRCUIT BREAKER) PATTERN ".

https://github.com/maxwoe/solidity_patterns/blob/master/security/EmergencyStop.sol

[N-12] Use descriptive names for Contracts and Libraries

This codebase will be difficult to navigate, as there are no descriptive naming conventions that specify which files should contain meaningful logic.

Prefixes should be added like this by filing:

Interface I_ absctract contracts Abs_ Libraries Lib_

We recommend that you implement this or a similar agreement.

[N-13] Use SMTChecker

The highest tier of smart contract behavior assurance is formal mathematical verification. All assertions that are made are guaranteed to be true across all inputs → The quality of your asserts is the quality of your verification.

https://twitter.com/0xOwenThurm/status/1614359896350425088?t=dbG9gHFigBX85Rv29lOjIQ&s=19

[N-14]  Add NatSpec Mapping comment

Description: Add NatSpec comments describing mapping keys and values

Context:

3 results

src/core/Factory.sol:
  39:   mapping(address => mapping(address => mapping(uint256 => mapping(uint256 => mapping(uint256 => address)))))

src/core/Lendgine.sol:
  56:   mapping(address => Position.Info) public override positions;

src/periphery/LiquidityManager.sol:
  69:   mapping(address => mapping(address => Position)) public positions;

[N-15] Require messages are too short or not

Description: The correct and clear error description explains to the user why the function reverts, but the error descriptions below in the project are not self-explanatory. These error descriptions are very important in the debug features of DApps like Tenderly. Error definitions should be added to the require block, not exceeding 32 bytes.


6 results - 4 files

src/core/libraries/PositionMath.sol:
  14:       require((z = x - uint256(-y)) < x, "LS");
  16:       require((z = x + uint256(y)) >= x, "LA");

src/libraries/SafeCast.sol:
   9:     require((z = uint120(y)) == y);
  16:     require(y < 2 ** 255);

src/periphery/LendgineRouter.sol:
  215:     // swap for required token0

src/periphery/SwapHelper.sol:
  116:         require(amountOutReceived == params.amount);

[N-16] Use underscores for number literals

2 results - 1 file

src/periphery/UniswapV2/libraries/UniswapV2Library.sol:
-  64:     uint256 denominator = (reserveIn * 1000) + amountInWithFee;
+ 64:     uint256 denominator = (reserveIn * 1_000) + amountInWithFee;

- 80:     uint256 numerator = reserveIn * amountOut * 1000;
+ 80:     uint256 numerator = reserveIn * amountOut * 1_000;

Description: There is occasions where certain number have been hardcoded, either in variable or in the code itself. Large numbers can become hard to read.

Recommendation: Consider using underscores for number literals to improve its readability.

[N-17] Showing the actual values of numbers in NatSpec comments makes checking and reading code easier

src/core/Lendgine.sol:
  251  
- 252:     uint256 dilutionLPRequested = (FullMath.mulDiv(borrowRate, _totalLiquidityBorrowed, 1e18) * timeElapsed) / 365 days;
+ 252:     uint256 dilutionLPRequested = (FullMath.mulDiv(borrowRate, _totalLiquidityBorrowed, 1e18) * timeElapsed) / 365 days; //  31_536_000 (365*24*60)

[N-18] Lines are too long

Lendgine.sol#L215 Lendgine.sol#L252 Lendgine.sol#L253 LiquidityManager.sol#L178 LiquidityManager.sol#L214 LiquidityManager.sol#L184 Position.sol#L83 Position.sol#L69 SwapHelper.sol#L42 SwapHelper.sol#L38 SwapHelper.sol#L69 SwapHelper.sol#L69

Description: It is generally recommended that lines in the source code should not exceed 80-120 characters. Today's screens are much larger, so in some cases it makes sense to expand that.The lines above should be split when they reach that length, as the files will most likely be on GitHub and GitHub always uses a scrollbar when the length is more than 164 characters.

(why-is-80-characters-the-standard-limit-for-code-width)[https://softwareengineering.stackexchange.com/questions/148677/why-is-80-characters-the-standard-limit-for-code-width]

Recommendation: Multiline output parameters and return statements should follow the same style recommended for wrapping long lines found in the Maximum Line Length section.

https://docs.soliditylang.org/en/v0.8.17/style-guide.html#introduction

thisFunctionCallIsReallyLong(
    longArgument1,
    longArgument2,
    longArgument3
);

#0 - c4-judge

2023-02-16T11:46:39Z

berndartmueller marked the issue as grade-a

Awards

45.4256 USDC - $45.43

Labels

bug
G (Gas Optimization)
grade-b
G-13

External Links

Gas Optimizations List

NumberOptimization DetailsContext
[G-01]Gas saving is achieved by removing the delete keyword (~30k)1
[G-02]Structs can be packed into fewer storage slots7
[G-03]Avoid contract existence checks by using solidity version 0.8.10 or later (2.4k gas)24
[G-04]Gas savings can be achieved by changing the model for assigning value to the structure (130 gas)1
[G-05]Gas saving by adding unchecked to getAmountOut function1
[G-06]State variable can be used instead of struct1
[G-07]Using storage instead of memory for structs/arrays saves gas2
[G-08]Setting the constructor to payable (65 gas)5
[G-09]x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables4
[G-10]>= costs less gas than >7
[G-11]Use nested if and, avoid multiple check combinations5
[G-12]Use double require instead of using &&2
[G-13]Sort Solidity operations using short-circuit mode2
[G-14]Optimize names to save gas (22 gas)All contracts
[G-15]Use a more recent version of solidity14

Total 15 issues

[G-01] Gas saving is achieved by removing the delete keyword (~30k)

30k gas savings were made by removing the delete keyword. The reason for using the delete keyword here is to reset the struct values (set to default value) in every operation. However, the struct values do not need to be zero each time the function is run. Therefore, the ``delete'' key word is unnecessary, if it is removed, around 30k gas savings will be achieved.

There is one instance of the subject:

src\core\Factory.sol:
  63   function createLendgine(
  64     address token0,
  65     address token1,
  66     uint8 token0Exp,
  67     uint8 token1Exp,
  68     uint256 upperBound
  69   )
  70     external
  71     override
  72     returns (address lendgine)
  73   {
  74     if (token0 == token1) revert SameTokenError();
  75     if (token0 == address(0) || token1 == address(0)) revert ZeroAddressError();
  76     if (getLendgine[token0][token1][token0Exp][token1Exp][upperBound] != address(0)) revert DeployedError();
  77     if (token0Exp > 18 || token0Exp < 6 || token1Exp > 18 || token1Exp < 6) revert ScaleError();
  78 
  79     parameters = 
  80       Parameters({token0: token0, token1: token1, token0Exp: token0Exp, token1Exp: token1Exp, upperBound: upperBound});
  81 
  82     lendgine = address(new Lendgine{ salt: keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)) }());
  83 
- 84:     delete parameters;
  85 
  86     getLendgine[token0][token1][token0Exp][token1Exp][upperBound] = lendgine;
  87     emit LendgineCreated(token0, token1, token0Exp, token1Exp, upperBound, lendgine);
  88   }

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

[G-02] Structs can be packed into fewer storage slots

Each saved slot can avoid an extra Gsset (20000 gas) for the initial setting of the build.

In LiquidityManager.sol contract: PairMintCallbackData, AddLiquidityParams, RemoveLiquidityParams

In LendgineRouter.sol contract: MintCallbackData, `MintParams, PairMintCallbackData, BurnParams``

If the structs uint256 token0Exp and uint256 token1Exp variables are changed to uint128 token0Exp and uint128 token1Exp, one slot for each specified structure can be won in total 7 slots.

7 results - 2 files:

src\periphery\LiquidityManager.sol:
   91  
   92:   struct PairMintCallbackData {
   93:     address token0;
   94:     address token1;
   95:     uint256 token0Exp;
   96:     uint256 token1Exp;
   97:     uint256 upperBound;
   98:     uint256 amount0;
   99:     uint256 amount1;
  100:     address payer;
  101:   }
  102 

  120:   struct AddLiquidityParams {
  121:     address token0;
  122:     address token1;
  123:     uint256 token0Exp;
  124:     uint256 token1Exp;
  125:     uint256 upperBound;
  126:     uint256 liquidity;
  127:     uint256 amount0Min;
  128:     uint256 amount1Min;
  129:     uint256 sizeMin;
  130:     address recipient;
  131:     uint256 deadline;
  132:   }
  

  187:   struct RemoveLiquidityParams {
  188:     address token0;
  189:     address token1;
  190:     uint256 token0Exp;
  191:     uint256 token1Exp;
  192:     uint256 upperBound;
  193:     uint256 size;
  194:     uint256 amount0Min;
  195:     uint256 amount1Min;
  196:     address recipient;
  197:     uint256 deadline;
  198:   }

https://github.com/code-423n4/2023-01-numoen/blob/main/src/periphery/LiquidityManager.sol#L92-L101 https://github.com/code-423n4/2023-01-numoen/blob/main/src/periphery/LiquidityManager.sol#L120-L132 https://github.com/code-423n4/2023-01-numoen/blob/main/src/periphery/LiquidityManager.sol#L187-L198

src/periphery/LendgineRouter.sol:
  73  
  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:   }

  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:   }

  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:   }


  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/main/src/periphery/LendgineRouter.sol#L74-L84 https://github.com/code-423n4/2023-01-numoen/blob/main/src/periphery/LendgineRouter.sol#L126-L139 https://github.com/code-423n4/2023-01-numoen/blob/main/src/periphery/LendgineRouter.sol#L175-L187 https://github.com/code-423n4/2023-01-numoen/blob/main/src/periphery/LendgineRouter.sol#L240-L254

Recommendation Code:

1 slot can be saved by packing the "MintCallbackData" structure as suggested below.

src\periphery\LendgineRouter.sol:
  73
  74:   struct MintCallbackData {
  75:     address token0;
  76:     address token1;
- 77:     uint256 token0Exp;
+ 77:     uint128 token0Exp;
- 78:     uint256 token1Exp;
+ 78:     uint128 token1Exp;
  79:     uint256 upperBound;
  80:     uint256 collateralMax;
  81:     SwapType swapType;
  82:     bytes swapExtraData;
  83:     address payer;
  84:   }

[G-03] Avoid contract existence checks by using solidity version 0.8.10 or later (2.4k gas)

Prior to 0.8.10 the compiler inserted extra code, including EXTCODESIZE (100 gas), to check for contract existence for external calls. In more recent solidity versions, the compiler will not insert these checks if the external call has a return value

24 results - 7 files:

src\core\Lendgine.sol:
  //@audit mintCallback()
  96:      IMintCallback(msg.sender).mintCallback(collateral, amount0, amount1, liquidity, data);

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

src\core\Pair.sol:
  //@audit pairMintCallback()
  77:      IPairMintCallback(msg.sender).pairMintCallback(liquidity, data);

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

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

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/main/src/periphery/LendgineRouter.sol#L147

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 accruePositionInterest()
  231:    ILendgine(params.lendgine).accruePositionInterest();

  ///@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/main/src/periphery/LiquidityManager.sol#L140

src\periphery\Payment.sol:
  ///@audit withdraw()
  30:     IWETH9(weth).withdraw(balanceWETH);

  ///@audit deposit()
  55:      IWETH9(weth).deposit{value: value}(); // wrap only what is needed to pay

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

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

src\periphery\SwapHelper.sol:
  ///@audit swap()
  88:     IUniswapV2Pair(pair).swap(amount0Out, amount1Out, params.recipient, bytes(""));

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

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/main/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L46

[G-04] Gas savings can be achieved by changing the model for assigning value to the structure (130 gas)

By changing the pattern of assigning value to the structure, gas savings of ~130 per sample are achieved. In addition, this use will save gas on distribution costs.

There are one examples of this issue:

src\core\Factory.sol:
  63   function createLendgine(

  79:     parameters = 
  80:       Parameters({token0: token0, token1: token1, token0Exp: token0Exp, token1Exp: token1Exp, upperBound: upperBound});

https://github.com/code-423n4/2023-01-numoen/blob/main/src/core/Factory.sol#L79-L80

The following model, which is more gas efficient, can be preferred to assign value to the building elements.

src\core\Factory.sol:
  63   function createLendgine(

- 79:     parameters = 
- 80:       Parameters({token0: token0, token1: token1, token0Exp: token0Exp, token1Exp: token1Exp, upperBound: upperBound});

+          parameters. token0 =  token0 ;
+          parameters. token1 = token1;
+          parameters. token0Exp = token0Exp;
+          parameters. token1Exp = token1Exp;
+          parameters. upperBound = upperBound;

[G-05] Gas saving by adding unchecked to getAmountOut function

Since the necessary 'require' checks are made in the 'getAmountOut' function, adding 'unchecked' will save approximately 100 gas.

src\periphery\UniswapV2\libraries\UniswapV2Library.sol:
  50    // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
  51:   function getAmountOut(
  52:     uint256 amountIn,
  53:     uint256 reserveIn,
  54:     uint256 reserveOut
  55:   )
  56:     internal
  57:     pure
  58:     returns (uint256 amountOut)
  59:   {
  60:     require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");
  61:     require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
  62:     uint256 amountInWithFee = amountIn * 997;
  63:     uint256 numerator = amountInWithFee * reserveOut;
  64:     uint256 denominator = (reserveIn * 1000) + amountInWithFee;
  +       unchecked { 
  65:     amountOut = numerator / denominator;
  +       }   
  66:   }

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

[G-06] State variable can be used instead of struct

Using the uint24 fee' state variable instead of the 'UniV3Data' structure saves gas.

1 result 1 file:

src\periphery\SwapHelper.sol:
  61:   struct UniV3Data {
  62:     uint24 fee;
  63:   }

https://github.com/code-423n4/2023-01-numoen/blob/main/src/periphery/SwapHelper.sol#L61-L63

[G-07] 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

2 results - 2 files:

src\core\Lendgine.sol:

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

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

src\core\libraries\Position.sol:

  47:     Position.Info memory _positionInfo = positionInfo;

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

[G-08] Setting the constructor to payable (65 gas)

You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0 and saves 13 gas on deployment with no security risks.

5 results - 5 files:

src\core\ImmutableState.sol:
  27:   constructor() {

github.com/code-423n4/2023-01-numoen/blob/main/src/core/ImmutableState.sol#L27

src\periphery\LendgineRouter.sol:
  49:   constructor(

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

src\periphery\LiquidityManager.sol:
  75:   constructor(address _factory, address _weth) Payment(_weth) {

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

src\periphery\Payment.sol:
  17:   constructor(address _weth) {

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

src\periphery\SwapHelper.sol:
  29:   constructor(address _uniswapV2Factory, address _uniswapV3Factory) {

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

Recommendation: Set the constructor to payable

[G-09] x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables

4 results - 1 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/main/src/core/Lendgine.sol#L91


src\core\Lendgine.sol:
- 91:     totalLiquidityBorrowed += liquidity;
+ 91:     totalLiquidityBorrowed = totalLiquidityBorrowed + liquidity;

x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables.

[G-10] >= 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

7 results - 4 files:

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/main/src/core/Lendgine.sol#L198

src\periphery\LiquidityManager.sol:
  241:    amount = params.amountRequested > position.tokensOwed ? position.tokensOwed : params.amountRequested;

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

src\periphery\SwapHelper.sol:
  42:     SafeTransferLib.safeTransfer(tokenIn, msg.sender, amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta));
  
  76:     amount = params.amount > 0
  77         ? UniswapV2Library.getAmountOut(uint256(params.amount), reserveIn, reserveOut)
  78         : UniswapV2Library.getAmountIn(uint256(-params.amount), reserveIn, reserveOut);
  
  81:     params.amount > 0 ? (uint256(params.amount), amount) : (amount, uint256(-params.amount));

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

src\periphery\UniswapV2\libraries\UniswapV2Library.sol:
  12:     (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

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

[G-11] Use nested if and, avoid multiple check combinations

Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.

5 results - 3 files:

src\core\Lendgine.sol:
   89:     if (totalSupply > 0 && totalLiquidityBorrowed == 0) revert CompleteUtilizationError();

  142:     if (totalLiquiditySupplied == 0 && totalPositionSize > 0) revert CompleteUtilizationError();

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

src\core\Pair.sol:
  100:     if (amount0 == 0 && amount1 == 0) revert InsufficientOutputError();

  117:     if (amount0Out == 0 && amount1Out == 0) revert InsufficientOutputError();

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

src\periphery\Payment.sol:
  53:     if (token == weth && address(this).balance >= value) {

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

Recomendation Code:

src\periphery\Payment.sol:
- 53:     if (token == weth && address(this).balance >= value) {
+          if (token == weth) {
+              if (address(this).balance >= value) {
+              }
+          }

[G-12] Use double require instead of using &&

Using double require instead of operator && can save more gas When having a require statement with 2 or more expressions needed, place the expression that cost less gas first.

2 results - 1 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/main/src/periphery/UniswapV2/libraries/UniswapV2Library.sol#L61

Recommendation Code:

src\periphery\UniswapV2\libraries\UniswapV2Library.sol:
- 61:     require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
+         require(reserveIn > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
+         require(reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");

[G-13] Sort Solidity operations using short-circuit mode

Short-circuiting is a solidity contract development model that uses OR/AND logic to sequence different cost operations. It puts low gas cost operations in the front and high gas cost operations in the back, so that if the front is low If the cost operation is feasible, you can skip (short-circuit) the subsequent high-cost Ethereum virtual machine operation.

//f(x) is a low gas cost operation //g(y) is a high gas cost operation //Sort operations with different gas costs as follows f(x) || g(y) f(x) && g(y)

2 results - 2 files:

src\libraries\Balance.sol:
  15:     if (!success || data.length < 32) revert BalanceReturnError();

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

src\periphery\Payment.sol:
  53:     if (token == weth && address(this).balance >= value) {

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

[G-14] Optimize names to save gas (22 gas)

Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions. 

Context:  All Contracts

Recommendation:  Find a lower method ID name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas For example, the function IDs in the LendgineRouter.sol contract will be the most used; A lower method ID may be given.

Proof of Consept: https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92

LendgineRouter.sol function names can be named and sorted according to METHOD ID

Sighash   |   Function Signature
========================
2dde7c24  =>  mintCallback(uint256,uint256,uint256,uint256,bytes)
0df9e5b8  =>  mint(MintParams)
ef4ae61c  =>  pairMintCallback(uint256,bytes)
4c5aaed3  =>  burn(BurnParams)

[G-15] Use a more recent version of solidity

In 0.8.15 the conditions necessary for inlining are relaxed. Benchmarks show that the change significantly decreases the bytecode size (which impacts the deployment cost) while the effect on the runtime gas usage is smaller.

In 0.8.17 prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call; Simplify the starting offset of zero-length operations to zero. More efficient overflow checks for multiplication.

14 results - 14 files:

src\core\Factory.sol:
  2: pragma solidity ^0.8.4;

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

src\core\ImmutableState.sol:
  2: pragma solidity ^0.8.0;

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

src\core\JumpRate.sol:
  2: pragma solidity ^0.8.0;

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

src\core\Lendgine.sol:
  2: pragma solidity ^0.8.4;

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

src\core\Pair.sol:
  2: pragma solidity ^0.8.4;

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

src\core\libraries\Position.sol:
  2: pragma solidity ^0.8.4;

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

src\libraries\Balance.sol:
  2: pragma solidity ^0.8.4;

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

src\libraries\SafeCast.sol:
  2: pragma solidity ^0.8.0;

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

src\periphery\LendgineRouter.sol:
  2: pragma solidity ^0.8.4;

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

src\periphery\LiquidityManager.sol:
  2: pragma solidity ^0.8.4;

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

src\periphery\Payment.sol:
  2: pragma solidity ^0.8.4;

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

src\periphery\SwapHelper.sol:
  2: pragma solidity ^0.8.4;

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

src\periphery\libraries\LendgineAddress.sol:
  2: pragma solidity >=0.5.0;

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

src\periphery\UniswapV2\libraries\UniswapV2Library.sol:
  1: pragma solidity >=0.8.0;

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

#0 - c4-judge

2023-02-16T11:16:13Z

berndartmueller marked the issue as grade-b

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