Numoen contest - matrix_0wl'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: 18/31

Findings: 2

Award: $187.91

QA:
grade-b
Gas:
grade-b

🌟 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-b
QA (Quality Assurance)
sponsor acknowledged
edited-by-warden
Q-01

Awards

142.4841 USDC - $142.48

External Links

Summary

Low Risk and Non-Critical Issues

Non-Critical Issues

Issue
NC-1USE OF BYTES.CONCAT() INSTEAD OF ABI.ENCODEPACKED(,)
NC-2SOLIDITY COMPILER VERSIONS MISMATCH
NC-3Use a more recent version of solidity
NC-4require() / revert() statements should have descriptive reason strings
NC-5For functions, follow solidity standard naming conventions
NC-6Lines are too long

| NC-6 | NOT VERIFIED INPUT |

[NC-1] USE OF BYTES.CONCAT() INSTEAD OF ABI.ENCODEPACKED(,)

Rather than using abi.encodePacked for appending bytes, since version 0.8.4, bytes.concat() is enabled

Since version 0.8.4 for appending bytes, bytes.concat() can be used instead of abi.encodePacked(,).

Proof Of Concept
File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

26:               keccak256(abi.encodePacked(token0, token1)),

[NC-2] SOLIDITY COMPILER VERSIONS MISMATCH

The project is compiled with different versions of Solidity, which is not recommended because it can lead to undefined behaviors.

It is better to use one Solidity compiler version across all contracts instead of different versions with different bugs and security checks.

Proof Of Concept
File: src/core/Factory.sol

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

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

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

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

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

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

2: pragma solidity >=0.5.0;
File: src/libraries/Balance.sol

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

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

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

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

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

2: pragma solidity ^0.8.4;
File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

1: pragma solidity >=0.8.0;
File: src/periphery/libraries/LendgineAddress.sol

2: pragma solidity >=0.5.0;

[NC-3] 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, newer version can be used (0.8.17)

[NC-4] require() / revert() statements should have descriptive reason strings

Proof Of Concept
File: src/libraries/SafeCast.sol

9:     require((z = uint120(y)) == y);

16:     require(y < 2 ** 255);
File: src/periphery/SwapHelper.sol

116:         require(amountOutReceived == params.amount);

[NC-5] For functions, follow solidity standard naming conventions

Solidity standard naming convention for internal and private functions: the mixedCase format starting with an underscore (_mixedCase starting with an underscore)

Proof Of Concept
File: src/core/JumpRate.sol

40:   function utilizationRate(uint256 borrowedLiquidity, uint256 totalLiquidity) private pure returns (uint256 rate) {
File: 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) {
File: src/core/libraries/Position.sol

38:       function update(

69:   function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) {

73:       function convertLiquidityToPosition(

86:     function convertPositionToLiquidity(
File: src/core/libraries/PositionMath.sol

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

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

8:   function toUint120(uint256 y) internal pure returns (uint120 z) {

15:   function toInt256(uint256 y) internal pure returns (int256 z) {
File: src/periphery/Payment.sol

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

69:   function swap(SwapType swapType, SwapParams memory params, bytes memory data) internal returns (uint256 amount) {
File: 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) {

36:       function getReserves(

51:     function getAmountOut(

69:     function getAmountIn(
File: src/periphery/libraries/LendgineAddress.sol

9:       function computeAddress(
Recommendation

Use solidity standard naming convention for internal and private functions

[NC-6] Lines are too long

Context:

Almost all Contracts

Description:

Usually lines in source code are limited to 80 characters.

Reference

Proof Of Concept
File: src/core/Factory.sol

39:   mapping(address => mapping(address => mapping(uint256 => mapping(uint256 => mapping(uint256 => address)))))

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();

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

82:     lendgine = address(new Lendgine{ salt: keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)) }());

87:     emit LendgineCreated(token0, token1, token0Exp, token1Exp, upperBound, lendgine);
File: src/core/ImmutableState.sol

33:     (token0, token1, _token0Exp, _token1Exp, upperBound) = Factory(msg.sender).parameters();
File: src/core/JumpRate.sol

13:   function getBorrowRate(uint256 borrowedLiquidity, uint256 totalLiquidity) public pure override returns (uint256 rate) {

40:   function utilizationRate(uint256 borrowedLiquidity, uint256 totalLiquidity) private pure returns (uint256 rate) {
File: src/core/Lendgine.sol

25:   event Mint(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to);

27:   event Burn(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to);

29:   event Deposit(address indexed sender, uint256 size, uint256 liquidity, address indexed to);

31:   event Withdraw(address indexed sender, uint256 size, uint256 liquidity, address indexed to);

33:   event AccrueInterest(uint256 timeElapsed, uint256 collateral, uint256 liquidity);

35:   event AccruePositionInterest(address indexed owner, uint256 rewardPerPosition);

89:     if (totalSupply > 0 && totalLiquidityBorrowed == 0) revert CompleteUtilizationError();

96:     IMintCallback(msg.sender).mintCallback(collateral, amount0, amount1, liquidity, data);

99:     if (balanceAfter < balanceBefore + collateral) revert InsufficientInputError();

105:   function burn(address to, bytes calldata data) external override nonReentrant returns (uint256 collateral) {

116:     SafeTransferLib.safeTransfer(token1, to, collateral); // optimistically transfer

138:     size = Position.convertLiquidityToPosition(liquidity, totalLiquiditySupplied, _totalPositionSize);

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

168:     liquidity = Position.convertPositionToLiquidity(size, totalLiquiditySupplied, _totalPositionSize);

175:     positions.update(msg.sender, -SafeCast.toInt256(size), rewardPerPositionStored);

194:   function collect(address to, uint256 collateralRequested) external override nonReentrant returns (uint256 collateral) {

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

213:   function convertLiquidityToShare(uint256 liquidity) public view override returns (uint256) {

215:     return _totalLiquidityBorrowed == 0 ? liquidity : FullMath.mulDiv(liquidity, totalSupply, _totalLiquidityBorrowed);

219:   function convertShareToLiquidity(uint256 shares) public view override returns (uint256) {

224:   function convertCollateralToLiquidity(uint256 collateral) public view override returns (uint256) {

229:   function convertLiquidityToCollateral(uint256 liquidity) public view override returns (uint256) {

248:     uint256 totalLiquiditySupplied = totalLiquidity + _totalLiquidityBorrowed; // SLOAD

250:     uint256 borrowRate = getBorrowRate(_totalLiquidityBorrowed, totalLiquiditySupplied);

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

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

257:     rewardPerPositionStored += FullMath.mulDiv(dilutionSpeculative, 1e18, totalPositionSize);
File: src/core/Pair.sol

8: import { IPairMintCallback } from "./interfaces/callback/IPairMintCallback.sol";

23:   event Burn(uint256 amount0Out, uint256 amount1Out, uint256 liquidity, address indexed to);

25:   event Swap(uint256 amount0Out, uint256 amount1Out, uint256 amount0In, uint256 amount1In, address indexed to);

53:   function invariant(uint256 amount0, uint256 amount1, uint256 liquidity) public view override returns (bool) {

81:     if (!invariant(_reserve0 + amount0In, _reserve1 + amount1In, _totalLiquidity + liquidity)) {

93:   function burn(address to, uint256 liquidity) internal returns (uint256 amount0, uint256 amount1) {

106:     if (!invariant(_reserve0 - amount0, _reserve1 - amount1, _totalLiquidity - liquidity)) revert InvariantError();

116:   function swap(address to, uint256 amount0Out, uint256 amount1Out, bytes calldata data) external override nonReentrant {

131:     if (!invariant(_reserve0 + amount0In - amount0Out, _reserve1 + amount1In - amount1Out, totalLiquidity)) {

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

136:     reserve1 = _reserve1 + SafeCast.toUint120(amount1In) - SafeCast.toUint120(amount1Out); // SSTORE
File: src/core/libraries/Position.sol

64:     if (tokensOwed > 0) positionInfo.tokensOwed = _positionInfo.tokensOwed + tokensOwed;

69:   function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) {

70:     return FullMath.mulDiv(position.size, rewardPerPosition - position.rewardPerPositionPaid, 1 ether);

83:       totalLiquiditySupplied == 0 ? liquidity : FullMath.mulDiv(liquidity, totalPositionSize, totalLiquiditySupplied);

95:     return FullMath.mulDiv(position, totalLiquiditySupplied, totalPositionSize);
File: src/libraries/Balance.sol

14:       token.staticcall(abi.encodeWithSelector(bytes4(keccak256(bytes("balanceOf(address)"))), address(this)));
File: src/periphery/LendgineRouter.sol

11: import { IPairMintCallback } from "../core/interfaces/callback/IPairMintCallback.sol";

20: contract LendgineRouter is Multicall, Payment, SelfPermit, SwapHelper, IMintCallback, IPairMintCallback {

25:   event Mint(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to);

27:   event Burn(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to);

100:       factory, decoded.token0, decoded.token1, decoded.token0Exp, decoded.token1Exp, decoded.upperBound

142:   function mint(MintParams calldata params) external payable checkDeadline(params.deadline) returns (uint256 shares) {

144:       factory, params.token0, params.token1, params.token0Exp, params.token1Exp, params.upperBound

190:   function pairMintCallback(uint256 liquidity, bytes calldata data) external override {

191:     PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData));

194:       factory, decoded.token0, decoded.token1, decoded.token0Exp, decoded.token1Exp, decoded.upperBound

213:     if (amount0 < decoded.amount0Min || amount1 < decoded.amount1Min) revert AmountError();

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

236:       SafeTransferLib.safeTransfer(decoded.token1, decoded.recipient, collateralOut);

257:   function burn(BurnParams calldata params) external payable checkDeadline(params.deadline) returns (uint256 amount) {

259:       factory, params.token0, params.token1, params.token0Exp, params.token1Exp, params.upperBound

262:     address recipient = params.recipient == address(0) ? address(this) : params.recipient;

264:     SafeTransferLib.safeTransferFrom(lendgine, msg.sender, lendgine, params.shares);
File: src/periphery/LiquidityManager.sol

9: import { IPairMintCallback } from "../core/interfaces/callback/IPairMintCallback.sol";

16: contract LiquidityManager is Multicall, Payment, SelfPermit, IPairMintCallback {

40:   event Collect(address indexed from, address indexed lendgine, uint256 amount, address indexed to);

105:     PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData));

108:       factory, decoded.token0, decoded.token1, decoded.token0Exp, decoded.token1Exp, decoded.upperBound

112:     if (decoded.amount0 > 0) pay(decoded.token0, decoded.payer, msg.sender, decoded.amount0);

113:     if (decoded.amount1 > 0) pay(decoded.token1, decoded.payer, msg.sender, decoded.amount1);

135:   function addLiquidity(AddLiquidityParams calldata params) external payable checkDeadline(params.deadline) {

137:       factory, params.token0, params.token1, params.token0Exp, params.token1Exp, params.upperBound

151:       amount0 = FullMath.mulDivRoundingUp(params.liquidity, r0, totalLiquidity);

152:       amount1 = FullMath.mulDivRoundingUp(params.liquidity, r1, totalLiquidity);

155:     if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert AmountError();

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

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

184:     emit AddLiquidity(msg.sender, lendgine, params.liquidity, size, amount0, amount1, params.recipient);

201:   function removeLiquidity(RemoveLiquidityParams calldata params) external payable checkDeadline(params.deadline) {

203:       factory, params.token0, params.token1, params.token0Exp, params.token1Exp, params.upperBound

206:     address recipient = params.recipient == address(0) ? address(this) : params.recipient;

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

209:     if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert AmountError();

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

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

220:     emit RemoveLiquidity(msg.sender, lendgine, liquidity, params.size, amount0, amount1, recipient);

230:   function collect(CollectParams calldata params) external payable returns (uint256 amount) {

233:     address recipient = params.recipient == address(0) ? address(this) : params.recipient;

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

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

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

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

247:     if (collectAmount != amount) revert CollectError(); // extra check for safety
File: src/periphery/Payment.sol

25:   function unwrapWETH(uint256 amountMinimum, address recipient) public payable {

35:   function sweepToken(address token, uint256 amountMinimum, address recipient) public payable {

45:     if (address(this).balance > 0) SafeTransferLib.safeTransferETH(msg.sender, address(this).balance);

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

6: import { IUniswapV3SwapCallback } from "./UniswapV3/interfaces/callback/IUniswapV3SwapCallback.sol";

38:   function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external override {

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

69:   function swap(SwapType swapType, SwapParams memory params, bytes memory data) internal returns (uint256 amount) {

71:       address pair = UniswapV2Library.pairFor(uniswapV2Factory, params.tokenIn, params.tokenOut);

74:         UniswapV2Library.getReserves(uniswapV2Factory, params.tokenIn, params.tokenOut);

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));

83:       (address token0,) = UniswapV2Library.sortTokens(params.tokenIn, params.tokenOut);

85:         params.tokenIn == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0));

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

99:           uniswapV3Factory, PoolAddress.getPoolKey(params.tokenIn, params.tokenOut, uniV3Data.fee)

115:         (amount, amountOutReceived) = zeroForOne ? (uint256(amount0), amount1) : (uint256(amount1), amount0);
File: 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) {

27:               hex"e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" // init code hash

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

47:     (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);

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

79:     require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
File: src/periphery/libraries/LendgineAddress.sol

7:     71_695_300_681_742_793_458_567_320_549_603_773_755_938_496_017_772_337_363_704_152_556_600_186_974;

28:               keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)),
Recommendation

The lines should be split when they reach that length.

Low Issues

Issue
L-1Potential underflow

[L-1] Potential underflow

In the line uint256 denominator = (reserveOut - amountOut) * 997; there is a potential for underflow if amountOut >= reserveOut, causing (reserveOut - amountOut) to result in a negative number. In this case, the value of denominator will be incorrect and cause an error.

Proof Of Concept
File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

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

#0 - c4-sponsor

2023-02-09T17:07:03Z

kyscott18 marked the issue as sponsor acknowledged

#1 - c4-judge

2023-02-16T11:26:23Z

berndartmueller marked the issue as grade-b

Awards

45.4256 USDC - $45.43

Labels

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

External Links

Summary

Gas Optimizations

IssueInstances
GAS-1<x> += <y>/<x> -= <y> costs more gas than <x> = <x> + <y>/<x> = <x> - <y> for state variables10
GAS-2Setting the constructor to payable5
GAS-3Internal functions only called once can be inlined to save gas2
GAS-4Optimize names to save gas9
GAS-5Proper data types17
GAS-6Reorder structure layout4
GAS-7Add unchecked {} for subtractions where the operands cannot underflow because of a previous require(), revert or if statement OR Using unchecked blocks to save gas5
GAS-8Using storage instead of memory for structs/arrays saves gas10
GAS-9Splitting require() statements that use && saves gas2
GAS-10Usage of uint/int smaller than 32 bytes (256 bits) incurs overhead3
GAS-11Upgrade to at least 0.8.42
GAS-12Public functions not called by the contract should be declared external instead2
GAS-13Not using the named return variables when a function returns, wastes deployment gas11

[GAS-1] <x> += <y>/<x> -= <y> costs more gas than <x> = <x> + <y>/<x> = <x> - <y> for state variables

Using compound assignment operators for state variables (like State += X or State -= X …) it’s more expensive than using operator assignment (like State = State + X or State = State - X …).

Proof Of Concept
File: src/core/Lendgine.sol

91:     totalLiquidityBorrowed += liquidity;

114:     totalLiquidityBorrowed -= liquidity;

176:     totalPositionSize -= size;

257:     rewardPerPositionStored += FullMath.mulDiv(dilutionSpeculative, 1e18, totalPositionSize);
File: src/periphery/LiquidityManager.sol

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

180:     position.size += size;

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

216:     position.size -= params.size;

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

242:     position.tokensOwed -= amount;

[GAS-2] Setting the constructor to payable

Saves ~13 gas per instance

Proof Of Concept
File: src/core/ImmutableState.sol

27:   constructor() {
File: src/periphery/LendgineRouter.sol

49:   constructor(
File: src/periphery/LiquidityManager.sol

75:   constructor(address _factory, address _weth) Payment(_weth) {
File: src/periphery/Payment.sol

17:   constructor(address _weth) {
File: src/periphery/SwapHelper.sol

29:   constructor(address _uniswapV2Factory, address _uniswapV3Factory) {

[GAS-3] Internal functions only called once can be inlined to save gas

Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.

Proof Of Concept
File: src/core/libraries/Position.sol

69:   function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) {
File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

17:   function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {

[GAS-4] 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. In this report 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 9 instances of this issue.

Proof Of Concept
File: src/core/Factory.sol

8: contract Factory is IFactory {
File: src/core/ImmutableState.sol

8: abstract contract ImmutableState is IImmutableState {
File: src/core/JumpRate.sol

6: abstract contract JumpRate is IJumpRate {
File: src/core/Lendgine.sol

17: contract Lendgine is ERC20, JumpRate, Pair, ILendgine {
File: src/core/Pair.sol

16: abstract contract Pair is ImmutableState, ReentrancyGuard, IPair {
File: src/periphery/LendgineRouter.sol

20: contract LendgineRouter is Multicall, Payment, SelfPermit, SwapHelper, IMintCallback, IPairMintCallback {
File: src/periphery/LiquidityManager.sol

16: contract LiquidityManager is Multicall, Payment, SelfPermit, IPairMintCallback {
File: src/periphery/Payment.sol

12: abstract contract Payment {
File: src/periphery/SwapHelper.sol

15: abstract contract SwapHelper is IUniswapV3SwapCallback {

[GAS-5] Proper data types

In Solidity, some data types are more expensive than others. It’s important to be aware of the most efficient type that can be used. Here are a few rules about data types.

Type uint should be used in place of type string whenever possible.

Type uint256 takes less gas to store than uint8

Type bytes should be used over byte[]

If the length of bytes can be limited, use the lowest amount possible from bytes1 to bytes32.

Type bytes32 is cheaper to use than type string and bytes.

If data can fit into 32 bytes, then you should use bytes32 datatype rather than bytes or strings as it is cheaper in solidity.

Proof Of Concept
File: src/core/Factory.sol

50:     uint128 token0Exp;

51:     uint128 token1Exp;

66:     uint8 token0Exp,

67:     uint8 token1Exp,
File: src/core/ImmutableState.sol

30:     uint128 _token0Exp;

31:     uint128 _token1Exp;
File: src/core/Pair.sol

40:   uint120 public override reserve0;

43:   uint120 public override reserve1;

71:     uint120 _reserve0 = reserve0; // SLOAD

72:     uint120 _reserve1 = reserve1; // SLOAD

94:     uint120 _reserve0 = reserve0; // SLOAD

95:     uint120 _reserve1 = reserve1; // SLOAD

119:     uint120 _reserve0 = reserve0; // SLOAD

120:     uint120 _reserve1 = reserve1; // SLOAD
File: src/periphery/SwapHelper.sol

62:     uint24 fee;
File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

20:       uint160( // extra cast for newer solidity
File: src/periphery/libraries/LendgineAddress.sol

22:       uint160(

[GAS-6] Reorder structure layout

Structures could be optimized moving the position of certain values in order to save a lot slots.

For example Enums are represented by integers; the possibility listed first by 0, the next by 1, and so forth. An enum type just acts like uintN, where N is the smallest legal value large enough to accomodate all the possibilities.

Source

Source

Proof Of Concept
File: src/periphery/LendgineRouter.sol

74:   struct MintCallbackData {
        address token0;
        address token1;
        uint256 token0Exp;
        uint256 token1Exp;
        uint256 upperBound;
        uint256 collateralMax;
    +   bytes swapExtraData;
        SwapType swapType;
    -   bytes swapExtraData;
        address payer;
  }

126:   struct MintParams {
        address token0;
        address token1;
        uint256 token0Exp;
        uint256 token1Exp;
        uint256 upperBound;
        uint256 amountIn;
        uint256 amountBorrow;
        uint256 sharesMin;
+       bytes swapExtraData;
        SwapType swapType;
-       bytes swapExtraData;
        address recipient;
        uint256 deadline;
  }

175:   struct PairMintCallbackData {
        address token0;
        address token1;
        uint256 token0Exp;
        uint256 token1Exp;
        uint256 upperBound;
        uint256 collateralMin;
        uint256 amount0Min;
        uint256 amount1Min;
+       bytes swapExtraData;
        SwapType swapType;
-       bytes swapExtraData;
        address recipient;
  }

240:   struct BurnParams {
        address token0;
        address token1;
        uint256 token0Exp;
        uint256 token1Exp;
        uint256 upperBound;
        uint256 shares;
        uint256 collateralMin;
        uint256 amount0Min;
        uint256 amount1Min;
+       bytes swapExtraData;
        SwapType swapType;
-       bytes swapExtraData;
        address recipient;
        uint256 deadline;
  }

[GAS-7] Add unchecked {} for subtractions where the operands cannot underflow because of a previous require(), revert or if statement OR Using unchecked blocks to save gas

Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn’t possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block

Resource

require(a <= b); x = b - a => require(a <= b); unchecked { x = b - a }

if(a <= b); x = b - a => if(a <= b); unchecked { x = b - a }

This will stop the check for overflow and underflow so it will save gas

Proof Of Concept
File: src/core/JumpRate.sol

20:     uint256 excessUtil = util - kink;
File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol

60:     require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");

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

78:     require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT");

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

[GAS-8] 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

Proof Of Concept
File: src/core/Lendgine.sol

167:     Position.Info memory positionInfo = positions[msg.sender]; // SLOAD
File: src/core/libraries/Position.sol

47:     Position.Info memory _positionInfo = positionInfo;

69:   function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) {
File: src/periphery/LendgineRouter.sol

97:     MintCallbackData memory decoded = abi.decode(data, (MintCallbackData));

191:     PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData));
File: src/periphery/LiquidityManager.sol

105:     PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData));

175:     Position memory position = positions[params.recipient][lendgine]; // SLOAD

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

235:     Position memory position = positions[msg.sender][params.lendgine]; // SLOAD
File: src/periphery/SwapHelper.sol

90:       UniV3Data memory uniV3Data = abi.decode(data, (UniV3Data));

[GAS-9] Splitting require() statements that use && saves gas

Instead of using operator && on a single require. Using a two require can save more gas.

Proof Of Concept
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");

[GAS-10] Usage of uint/int 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. 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.

https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html

Proof Of Concept
File: src/core/Factory.sol

66:     uint8 token0Exp,

67:     uint8 token1Exp,
File: src/periphery/SwapHelper.sol

62:     uint24 fee;

[GAS-11] Upgrade to at least 0.8.4

Use pragma solidity bigger than 0.8.0 - Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks for free! Safemath by default from 0.8.0 (can be more gas efficient than some library-based safemath.)

Low-level inliner from 0.8.2, leads to cheaper runtime gas. Especially relevant when the contract has small functions. For example, OpenZeppelin libraries typically have a lot of small helper functions and if they are not inlined, they cost an additional 20 to 40 gas because of 2 extra jump instructions and additional stack operations needed for function calls.

Optimizer improvements in packed structs: Before 0.8.3, storing packed structs, in some cases, used additional storage read operation. After EIP-2929, if the slot was already cold, this means unnecessary stack operations and extra deploy time costs. However, if the slot was already warm, this means an additional cost of 100 gas alongside the same unnecessary stack operations and extra deploy time costs.)

Custom errors 24 from 0.8.4, leads to cheaper deploy time cost and run time cost. Note: the run time cost is only relevant when the revert condition is met. In short, replace revert strings with custom errors.

Source

Proof Of Concept
File: src/core/libraries/PositionMath.sol

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

2: pragma solidity >=0.5.0;

Upgrade to at least 0.8.4

[GAS-12] Public functions not called by the contract should be declared external instead

Contracts are allowed to override their parents’ functions and change the visibility from external to public and can save gas by doing so.

Proof Of Concept
File: src/periphery/Payment.sol

25:   function unwrapWETH(uint256 amountMinimum, address recipient) public payable {

35:   function sweepToken(address token, uint256 amountMinimum, address recipient) public payable {

[GAS-13] Not using the named return variables when a function returns, wastes deployment gas

Do not use return at the end of the function:

Proof Of Concept
File: src/core/Factory.sol

63:       function createLendgine(
File: src/core/Lendgine.sol

71:      function mint(

105:   function burn(address to, bytes calldata data) external override nonReentrant returns (uint256 collateral) {

125:       function deposit(

152:       function withdraw(

194:   function collect(address to, uint256 collateralRequested) external override nonReentrant returns (uint256 collateral) {
File: src/core/Pair.sol

93:   function burn(address to, uint256 liquidity) internal returns (uint256 amount0, uint256 amount1) {
File: src/periphery/LendgineRouter.sol

142:   function mint(MintParams calldata params) external payable checkDeadline(params.deadline) returns (uint256 shares) {

257:   function burn(BurnParams calldata params) external payable checkDeadline(params.deadline) returns (uint256 amount) {
File: src/periphery/LiquidityManager.sol

230:   function collect(CollectParams calldata params) external payable returns (uint256 amount) {
File: src/periphery/SwapHelper.sol

69:   function swap(SwapType swapType, SwapParams memory params, bytes memory data) internal returns (uint256 amount) {

#0 - c4-judge

2023-02-16T11:18:43Z

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