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
Rank: 18/31
Findings: 2
Award: $187.91
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: CodingNameKiki
Also found by: 0xAgro, 0xSmartContract, IllIllI, Rolezn, SleepingBugs, btk, chrisdior4, matrix_0wl
142.4841 USDC - $142.48
Issue | |
---|---|
NC-1 | USE OF BYTES.CONCAT() INSTEAD OF ABI.ENCODEPACKED(,) |
NC-2 | SOLIDITY COMPILER VERSIONS MISMATCH |
NC-3 | Use a more recent version of solidity |
NC-4 | require() Â /Â revert() Â statements should have descriptive reason strings |
NC-5 | For functions, follow solidity standard naming conventions |
NC-6 | Lines are too long |
| NC-6 | NOT VERIFIED INPUT |
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(,)
.
File: src/periphery/UniswapV2/libraries/UniswapV2Library.sol 26: keccak256(abi.encodePacked(token0, token1)),
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.
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;
All Contracts
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
Old version of Solidity is used, newer version can be used (0.8.17)
require()
 / revert()
 statements should have descriptive reason stringsFile: 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);
Solidity standard naming convention for internal and private functions: the mixedCase format starting with an underscore (_mixedCase starting with an underscore)
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(
Use solidity standard naming convention for internal and private functions
Almost all Contracts
Usually lines in source code are limited to 80 characters.
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)),
The lines should be split when they reach that length.
Issue | |
---|---|
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.
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
🌟 Selected for report: NoamYakov
Also found by: 0xSmartContract, 0xackermann, Aymen0909, Deivitto, Diana, IllIllI, RaymondFam, ReyAdmirado, Rolezn, antonttc, arialblack14, c3phas, cryptostellar5, matrix_0wl, nadin, oyc_109
45.4256 USDC - $45.43
Issue | Instances | |
---|---|---|
GAS-1 | <x> += <y> /<x> -= <y> costs more gas than <x> = <x> + <y> /<x> = <x> - <y> for state variables | 10 |
GAS-2 | Setting the constructor to payable | 5 |
GAS-3 | Internal functions only called once can be inlined to save gas | 2 |
GAS-4 | Optimize names to save gas | 9 |
GAS-5 | Proper data types | 17 |
GAS-6 | Reorder structure layout | 4 |
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 | 5 |
GAS-8 | Using storage instead of memory for structs/arrays saves gas | 10 |
GAS-9 | Splitting require() statements that use && saves gas | 2 |
GAS-10 | Usage of uint /int smaller than 32 bytes (256 bits) incurs overhead | 3 |
GAS-11 | Upgrade to at least 0.8.4 | 2 |
GAS-12 | Public functions not called by the contract should be declared external instead | 2 |
GAS-13 | Not using the named return variables when a function returns, wastes deployment gas | 11 |
<x> += <y>
/<x> -= <y>
costs more gas than <x> = <x> + <y>
/<x> = <x> - <y>
for state variablesUsing 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
…).
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;
Saves ~13 gas per instance
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) {
Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.
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) {
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.
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 {
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.
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(
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.
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; }
unchecked {}
for subtractions where the operands cannot underflow because of a previous require()
, revert
or if
statement OR Using unchecked blocks to save gasSolidity 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
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
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");
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
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));
Instead of using operator &&
on a single require
. Using a two require
can save more gas.
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");
uint
/int
smaller than 32 bytes (256 bits) incurs overheadWhen 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
File: src/core/Factory.sol 66: uint8 token0Exp, 67: uint8 token1Exp,
File: src/periphery/SwapHelper.sol 62: uint24 fee;
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.
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
Contracts are allowed to override their parents’ functions and change the visibility from external to public and can save gas by doing so.
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 {
Do not use return at the end of the function:
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