Platform: Code4rena
Start Date: 01/08/2023
Pot Size: $91,500 USDC
Total HM: 14
Participants: 80
Period: 6 days
Judge: gzeon
Total Solo HM: 6
Id: 269
League: ETH
Rank: 11/80
Findings: 4
Award: $1,076.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: dd0x7e8
Also found by: Bughunter101, Fulum, Kaysoft, MatricksDeCoder, SanketKogekar, Sathish9098, T1MOH, Udsen, debo, fatherOfBlocks, grearlake, hpsb, j4ld1na, josephdara, parsely, pep7siup, piyushshukla, ravikiranweb3, shirochan
12.8772 USDC - $12.88
https://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/helper/V3Proxy.sol#L156 https://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/helper/V3Proxy.sol#L174 https://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/helper/V3Proxy.sol#L192
The payable(msg.sender).call{value: amountOut}("") function does not check the status of the call, so it is possible to lose funds if the call fails.
FILE: 2023-08-goodentry/contracts/helper/V3Proxy.sol 156: msg.sender.call{value: msg.value - amounts[0]}(""); 174: payable(msg.sender).call{value: amountOut}(""); 192: payable(msg.sender).call{value: amounts[1]}("");
The call function is a low-level function that allows you to send a message to another contract. The call function does not check the status of the call, so it is possible for the call to fail for a variety of reasons
Manual Audit
Implement return value check to avoid funds lose
+ (bool sent,) = payable(msg.sender).call{value: amountOut}(""); + require(sent, " Call failed ")
Access Control
#0 - c4-pre-sort
2023-08-09T02:06:28Z
141345 marked the issue as duplicate of #481
#1 - c4-pre-sort
2023-08-09T09:26:06Z
141345 marked the issue as duplicate of #83
#2 - c4-judge
2023-08-20T17:11:10Z
gzeon-c4 changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-08-20T17:11:31Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: Team_FliBit
Also found by: 0x70C9, 3docSec, 8olidity, DavidGiladi, Krace, LokiThe5th, Rolezn, Sathish9098, UniversalCrypto, banpaleo5, catellatech, digitizeworx, fatherOfBlocks, hpsb, j4ld1na, josephdara, kutugu, niser93, nonseodion, oakcobalt, osmanozdemir1, pep7siup, ravikiranweb3, said, sivanesh_808
212.1661 USDC - $212.17
push()ed
but not pop()ed
Array entries are added but are never removed. Consider whether this should be the case, or whether there should be a maximum, or whether old entries should be removed. Cases where there are specific potential problems will be flagged separately under a different issue.
FILE: 2023-08-goodentry/contracts/GeVault.sol 121: if (ticks.length == 0) ticks.push(t); 130: ticks.push(TokenisableRange(tr)); 152: ticks.push(ticks[ticks.length-1]);
FILE: Breadcrumbs2023-08-goodentry/contracts/RangeManager.sol 78: stepList.push( Step(startX10, endX10) ); 80: tokenisedRanges.push( TokenisableRange(address(trbp)) ); 82: tokenisedTicker.push( TokenisableRange(address(trbp)) );
FILE: Breadcrumbs2023-08-goodentry/contracts/RoeRouter.sol 76: pools.push(RoePool(lendingPoolAddressProvider, token0, token1, ammRouter, false));
Use pop()
method to remove elements
contract-existence
checks before low-level callsLow-level calls return success if there is no code present at the specified address. In addition to the zero-address checks, add a check to verify that <address>.code.length > 0
FILE: Breadcrumbs2023-08-goodentry/contracts/helper/V3Proxy.sol 174: payable(msg.sender).call{value: amountOut}(""); 192: payable(msg.sender).call{value: amounts[1]}("");
Add <address>.code.length > 0
this check
recipient
may consume all transaction gasThere is no limit specified on the amount of gas used, so the recipient can use up all of the transaction's gas, causing it to revert.
FILE: Breadcrumbs2023-08-goodentry/contracts/helper/V3Proxy.sol 174: payable(msg.sender).call{value: amountOut}(""); 192: payable(msg.sender).call{value: amounts[1]}("");
Use addr.call{gas: <amount>}("")
or this library instead.
Fees/rates should be required to be below 100%, preferably at a much lower limit, to ensure users don't have to monitor the blockchain for changes prior to using the protocol
/// @notice Max vault TVL with 8 decimals
FILE: Breadcrumbs2023-08-goodentry/contracts/GeVault.sol 190: function setTvlCap(uint newTvlCap) public onlyOwner { 191: tvlCap = newTvlCap; 192: emit SetTvlCap(newTvlCap); 193: }
Add sanity checks
(Min,Max,>0) when assigning uint
values
A copy-paste error or a typo may end up bricking protocol functionality, or sending tokens to an address with no known private key. Consider implementing a two-step procedure for updating protocol addresses, where the recipient is set as pending, and must 'accept' the assignment by making an affirmative call. A straight forward way of doing this would be to have the target contracts implement EIP-165, and to have the 'set' functions ensure that the recipient is of the right interface type.
FILE: 2023-08-goodentry/contracts/RoeRouter.sol function setTreasury(address newTreasury) public onlyOwner { require(newTreasury != address(0x0), "Invalid address"); treasury = newTreasury; emit UpdateTreasury(newTreasury); }
FILE: Breadcrumbs2023-08-goodentry/contracts/GeVault.sol function setTreasury(address newTreasury) public onlyOwner { treasury = newTreasury; emit SetTreasury(newTreasury); }
Add two-step procedure
when updating protocol addresses
safeApprove()
is deprecatedDeprecated in favor of safeIncreaseAllowance() and safeDecreaseAllowance(). If only setting the initial allowance to the value that means infinite, safeIncreaseAllowance() can be used instead. The function may currently work, but if a bug is found in this version of OpenZeppelin, and the version that you're forced to upgrade to no longer has this function, you'll encounter unnecessary delays in porting and testing replacement contracts.
FILE: 2023-08-goodentry/contracts/helper/V3Proxy.sol 116: ogInAsset.safeApprove(address(ROUTER), amountIn); 120: ogInAsset.safeApprove(address(ROUTER), 0); 128: ogInAsset.safeApprove(address(ROUTER), amountInMax); 133: ogInAsset.safeApprove(address(ROUTER), 0); 165: ogInAsset.safeApprove(address(ROUTER), amountInMax); 169: ogInAsset.safeApprove(address(ROUTER), 0); 183: ogInAsset.safeApprove(address(ROUTER), amountIn); 187: ogInAsset.safeApprove(address(ROUTER), 0);
Use safeIncreaseAllowance()
and safeDecreaseAllowance()
totalSupply()
being zero will result in a division by zero, causing the transaction to fail. The function should instead special-case this scenario, and avoid reverting.
FILE: 2023-08-goodentry/contracts/GeVault.sol 221: uint valueX8 = vaultValueX8 * liquidity / totalSupply();
FILE: Breadcrumbs2023-08-goodentry/contracts/TokenisableRange.sol 371: (token0Amount, token1Amount) = LiquidityAmounts.getAmountsForLiquidity( sqrtPriceX96, TickMath.getSqrtRatioAtTick(lowerTick), TickMath.getSqrtRatioAtTick(upperTick), uint128 ( uint(liquidity) * amount / totalSupply() ) );
totalSupply()
must be non zero
[L-2] Some tokens may revert when zero value transfers are made
In spite of the fact that EIP-20 states that zero-valued transfers must be accepted, some tokens, such as LEND will revert if this is attempted, which may cause transactions that involve other tokens (such as batch operations) to fully revert. Consider skipping the transfer if the amount is zero, which will also save gas.
FILE: 2023-08-goodentry/contracts/GeVault.sol 227: ERC20(token).safeTransfer(treasury, fee); 235: ERC20(token).safeTransfer(msg.sender, bal); 267: ERC20(token).safeTransfer(treasury, fee);
Check zero amount
before call transfer
function
treasuryFee
may cause problem in futureThe fee may be difficult to change, which can make it difficult to adapt to changes in the market
FILE: 2023-08-goodentry/contracts/TokenisableRange.sol 61: uint constant public treasuryFee = 20;
Make sure treasuryFee
easy to change when necessary
view
when evert states are not changedstatus
is not checked when accessing pools
The deprecatePool function does not check the deprecated status of the pool before accessing it. This means that it is possible to call the getPool function with a deprecated pool id, and the function will return the pool's data, even though the pool is deprecated
FILE: 2023-08-goodentry/contracts/PositionManager/PositionManager.sol function getPoolAddresses(uint poolId) internal view returns( ILendingPool lp, IPriceOracle oracle, IUniswapV2Router01 router, address token0, address token1) { (address lpap, address _token0, address _token1, address r, ) = ROEROUTER.pools(poolId); token0 = _token0; token1 = _token1; lp = ILendingPool(ILendingPoolAddressesProvider(lpap).getLendingPool()); oracle = IPriceOracle(ILendingPoolAddressesProvider(lpap).getPriceOracle()); router = IUniswapV2Router01(r); } FILE: Breadcrumbs2023-08-goodentry/contracts/RoeRouter.sol function deprecatePool(uint poolId) public onlyOwner { pools[poolId].isDeprecated = true; emit DeprecatePool(poolId); }
Add check to ensure the pool is deprecated or not
payable(address).call()
payable(address).call()
is a low-level method that can be used to send ether to a contract, but it has some limitations and risks as you've pointed out. One of the primary risks of using payable(address).call()
is that it doesn't guarantee that the contract's payable function will be called successfully. This can lead to funds being lost or stuck in the contract
The contract does not have a payable callback The contract’s payable callback spends more than 2300 gas (which is only enough to emit something) The contract is called through a proxy which itself uses up the 2300 gas Use OpenZeppelin’s Address.sendValue() instead
FILE: Breadcrumbs2023-08-goodentry/contracts/helper/V3Proxy.sol 174: payable(msg.sender).call{value: amountOut}(""); 192: payable(msg.sender).call{value: amounts[1]}("");
Use OpenZeppelin’s Address.sendValue()
Anyone can call and can initiate the liquidations . This may creates unintended consequences.
FILE: Breadcrumbs2023-08-goodentry/contracts/PositionManager/OptionsPositionManager.sol function liquidate ( uint poolId, address user, address[] memory options, uint[] memory amounts, address collateralAsset ) external
Add control like onlyOwner()
oracle.getAssetPrice(collateralAsset)
this will revert some cases if collateralAsset
is not existsdeadline
and no slippage protections
when swapping tokensThe swapTokens function does not have a deadline or slippage protection. This means that there is no guarantee that the swap will be executed, and the user may lose money if the price of the tokens changes significantly during the swap.
A deadline is a time limit that is set for a transaction. If the transaction is not executed before the deadline, it is reverted. This can be useful for preventing users from losing money if the price of the tokens changes significantly during the swap.
Slippage protection is a feature that limits the amount of slippage that can occur during a swap. Slippage is the difference between the expected price of a swap and the actual price of the swap. If the price of the tokens changes significantly during the swap, the user may lose money due to slippage
Add minOutAmount
and deadline
in swapTokens()
functions
#0 - 141345
2023-08-10T09:57:25Z
#1 - c4-judge
2023-08-20T16:35:14Z
gzeon-c4 marked the issue as grade-a
🌟 Selected for report: JCK
Also found by: 0xAnah, 0xhex, 0xta, DavidGiladi, K42, Rageur, Raihan, ReyAdmirado, Rolezn, SAQ, SY_S, Sathish9098, dharma09, hunter_w3b, matrix_0wl, naman1778, petrichor, wahedtalash77
190.0322 USDC - $190.03
Issue Count | Issues | Instances | Gas Saves |
---|---|---|---|
[G-1] | Using calldata instead of memory for read-only arguments in external functions saves gas | 7 | 1974 |
[G-2] | State variables can be packed into fewer storage slots | 2 | 4000 |
[G-3] | Structs can be packed into fewer storage slots | 2 | 4000 |
[G-4] | Using bools for storage incurs overhead | 3 | 60000 |
[G-5] | Functions guaranteed to revert when called by normal users can be marked payable | 13 | 273 |
[G-6] | Multiple accesses of a mapping/array should use a local variable cache | 10 | 1000 |
[G-7] | State variable should be cached inside the loop | 1 | 100 |
[G-8] | IF’s/require() statements that check input arguments should be at the top of the function | 1 | 400 |
[G-9] | Consider using the view or pure keywords for functions that don't modify state to save gas | - | - |
calldata
instead of memory for read-only arguments in external functions saves gas1974 GAS
When a function with a memory array is called externally, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length). Using calldata directly, obliviates the need for such a loop in the contract code and runtime execution. Note that even if an interface defines a function as having memory arguments, it’s still valid for implementation contracs to use calldata arguments instead.
If the array is passed to an internal function which passes the array to another internal function where the array is modified and therefore memory is used in the external call, it’s still more gass-efficient to use calldata when the external function uses modifiers, since the modifiers may prevent the internal functions from being called. Structs have the same overhead as an array of length one
Note that I’ve also flagged instances where the function is public but can be marked as external since it’s not called by the contract, and cases where a constructor is involved
options, amounts, sourceSwap
can be changed to calldata instead of memory. Because read-only arguments : Saves 846 GAS
FILE: 2023-08-goodentry/contracts/PositionManager/OptionsPositionManager.sol 159: function buyOptions( 160: uint poolId, + 161: address[] calldata options, - 161: address[] memory options, + 162: uint[] calldata amounts, - 162: uint[] memory amounts, + 163: address[] memory sourceSwap - 163: address[] memory sourceSwap 164: ) 165: external 166: {
options, amounts
can be changed to calldata
instead of memory
. Because read-only arguments: Saves 564 GAS
FILE: 2023-08-goodentry/contracts/PositionManager/OptionsPositionManager.sol 189: function liquidate ( 190: uint poolId, 191: address user, + 192: address[] memory options, - 192: address[] memory options, + 193: uint[] memory amounts, - 193: uint[] memory amounts, 194: address collateralAsset 195: ) 196: external 197: {
startName, endName
can be changed to calldata
instead of memory
. Because read-only arguments : Saves 564 GAS
FILE: 2023-08-goodentry/contracts/RangeManager.sol - 75: function generateRange(uint128 startX10, uint128 endX10, string memory startName, string memory endName, address beacon) external onlyOwner { + 75: function generateRange(uint128 startX10, uint128 endX10, string calldata startName, string calldata endName, address beacon) external onlyOwner {
4000 GAS, 2 SLOT
The EVM works with 32 byte words. Variables less than 32 bytes can be declared next to eachother in storage and this will pack the values together into a single 32 byte storage slot (if the values combined are <= 32 bytes). If the variables packed together are retrieved together in functions we will effectively save ~2000 gas with every subsequent SLOAD for that storage slot. This is due to us incurring a Gwarmaccess (100 gas) versus a Gcoldsload (2100 gas).
baseFeeX4
can be declared uint96 instead of uint
: Saves 2000 GAS
, 1 SLOT
baseFeeX4
value is not exceed 1e4
because require(newBaseFeeX4 < 1e4, "GEV: Invalid Base Fee");
this check
FILE: Breadcrumbs2023-08-goodentry/contracts/GeVault.sol 48: ERC20 public token0; + 51: /// @notice Pool base fee + 52: uint96 public baseFeeX4 = 20; 49: ERC20 public token1; 50: bool public isEnabled = true; - 51: /// @notice Pool base fee - 52: uint public baseFeeX4 = 20; 53: /// @notice Max vault TVL with 8 decimals 54: uint public tvlCap = 1e12;
lowerTick,upperTick,feeTier,liquidity
can be packed together : Saves 2000 GAS, 1 SLOT
FILE: Breadcrumbs2023-08-goodentry/contracts/TokenisableRange.sol int24 public lowerTick; int24 public upperTick; uint24 public feeTier; + uint128 public liquidity; uint256 public tokenId; uint256 public fee0; uint256 public fee1; struct ASSET { ERC20 token; uint8 decimals; } ASSET public TOKEN0; ASSET public TOKEN1; IAaveOracle public ORACLE; string _name; string _symbol; enum ProxyState { INIT_PROXY, INIT_LP, READY } ProxyState public status; address private creator; - uint128 public liquidity; // @notice deprecated, keep to avoid beacon storage slot overwriting errors address public TREASURY_DEPRECATED = 0x22Cc3f665ba4C898226353B672c5123c58751692; uint public treasuryFee_deprecated = 20;
4000 GAS
, 2 SLOTs
Each slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct.
deadline
can be downcasted to uint96 instead of uint256 : saves 4000 GAS, 2 SLOTs
.The current timestamp is 2023-08-07 04:44:39 PST. The maximum timestamp that can be stored in a uint96 is 2**96 - 1, which is about 1.8446744073709552e+18 seconds. This is more than 276 years, so it will take at least 276 years for the block timestamp to overflow when stored in a uint96
FILE: 2023-08-goodentry/contracts/helper/V3Proxy.sol 10: struct ExactInputSingleParams { 11: address tokenIn; 12: address tokenOut; 13: uint24 fee; 14: address recipient; + 15: uint96 deadline; - 15: uint256 deadline; 16: uint256 amountIn; 17: uint256 amountOutMinimum; 18: uint160 sqrtPriceLimitX96; 19: } 22: struct ExactOutputSingleParams { 23: address tokenIn; 24: address tokenOut; 25: uint24 fee; 26: address recipient; + 27: uint96 deadline; - 27: uint256 deadline; 28: uint256 amountOut; 29: uint256 amountInMaximum; 30: uint160 sqrtPriceLimitX96; 31: }
60000 GAS
, 3 Instances
// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from false to true, after having been true in the past.
FILE: Breadcrumbs2023-08-goodentry/contracts/GeVault.sol 50: bool public isEnabled = true; 64: bool public baseTokenIsToken0;
FILE: 2023-08-goodentry/contracts/helper/V3Proxy.sol 65: bool acceptPayable;
Saves 273 GAS
, 13 Instances
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost
101: function setEnabled(bool _isEnabled) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L101-L101
108: function setTreasury(address newTreasury) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L108-L108
116: function pushTick(address tr) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L116-L116
137: function shiftTick(address tr) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L137-L137
167: function modifyTick(address tr, uint index) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L167-L167
183: function setBaseFee(uint newBaseFeeX4) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L183-L183
191: function setTvlCap(uint newTvlCap) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L191-L191
94: function emergencyWithdraw(ERC20 token) onlyOwner external
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/helper/V3Proxy.sol#L94-L94
75: function generateRange(uint128 startX10, uint128 endX10, string memory startName, string memory endName, address beacon) external onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/RangeManager.sol#L75-L75
95: function initRange(address tr, uint amount0, uint amount1) external onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/RangeManager.sol#L95-L95
48: function deprecatePool(uint poolId) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/RoeRouter.sol#L48-L48
59: function addPool( 60: address lendingPoolAddressProvider, 61: address token0, 62: address token1, 63: address ammRouter 64: ) 65: public onlyOwner 66: returns (uint poolId) 67:
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/RoeRouter.sol#L65-L65
24: function setHardcodedPrice(int256 _hardcodedPrice) external onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/helper/FixedOracle.sol#L24-L24
1100 GAS
, 11 SLODs
The instances below point to the second+ access of a value inside a mapping/array, within a function. Caching a mapping’s value in a local storage or calldata variable when the value is accessed multiple times, saves ~42 gas per access due to not having to recalculate the key’s keccak256 hash (Gkeccak256 - 30 gas) and that calculation’s associated stack operations. Caching an array’s struct avoids recalculating the array offsets into memory/calldata
ticks[ticks.length-1]
, ticks[0]
should be cached with stack variable : Saves 200 GAS, 2 SLOD
FILE: Breadcrumbs2023-08-goodentry/contracts/GeVault.sol 122: else { 123: // Check that tick is properly ordered + TokenisableRange ticks_ = ticks[ticks.length-1]; 124: if (baseTokenIsToken0) - 125: require( t.lowerTick() > ticks[ticks.length-1].upperTick(), "GEV: Push Tick Overlap"); + 125: require( t.lowerTick() > ticks_.upperTick(), "GEV: Push Tick Overlap"); 126: else - 127: require( t.upperTick() < ticks[ticks.length-1].lowerTick(), "GEV: Push Tick Overlap"); + 127: require( t.upperTick() < ticks_ .lowerTick(), "GEV: Push Tick Overlap"); 128: 129: ticks.push(TokenisableRange(tr)); 145: else { 146: // Check that tick is properly ordered + TokenisableRange ticks_ = ticks[0]; 147: if (!baseTokenIsToken0) - 148: require( t.lowerTick() > ticks[0].upperTick(), "GEV: Shift Tick Overlap"); + 148: require( t.lowerTick() > ticks_.upperTick(), "GEV: Shift Tick Overlap"); 149: else - 150: require( t.upperTick() < ticks[0].lowerTick(), "GEV: Shift Tick Overlap"); + 150: require( t.upperTick() < ticks_.lowerTick(), "GEV: Shift Tick Overlap");
tokenisedTicker[step]
,tokenisedRanges[ tokenisedRanges.length - 1 ]
,tokenisedTicker[step]
should be cached : Saves 800 GAS
, 8 SLOD
FILE: Breadcrumbs2023-08-goodentry/contracts/RangeManager.sol + TokenisableRange step_ = tokenisedRanges[step] ; - 111: trAmt = ERC20(LENDING_POOL.getReserveData(address(tokenisedRanges[step])).aTokenAddress).balanceOf(msg.sender); + 111: trAmt = ERC20(LENDING_POOL.getReserveData(address(step_)).aTokenAddress).balanceOf(msg.sender); 112: if (trAmt > 0) { 113: LENDING_POOL.PMTransfer( - 114: LENDING_POOL.getReserveData(address(tokenisedRanges[step])).aTokenAddress, + 114: LENDING_POOL.getReserveData(address(step_)).aTokenAddress, 115: msg.sender, 116: trAmt 117: ); - 118: trAmt = LENDING_POOL.withdraw(address(tokenisedRanges[step]), type(uint256).max, address(this)); + 118: trAmt = LENDING_POOL.withdraw(address(step_), type(uint256).max, address(this)); - 119: tokenisedRanges[step].withdraw(trAmt, 0, 0); + 119: step_.withdraw(trAmt, 0, 0); - 120: emit Withdraw(msg.sender, address(tokenisedRanges[step]), trAmt); + 120: emit Withdraw(msg.sender, address(step_), trAmt); 121: } 122: + TokenisableRange stepTicker_ = tokenisedTicker[step] ; - 123: trAmt = ERC20(LENDING_POOL.getReserveData(address(tokenisedTicker[step])).aTokenAddress).balanceOf(msg.sender); + 123: trAmt = ERC20(LENDING_POOL.getReserveData(address(stepTicker_)).aTokenAddress).balanceOf(msg.sender); 124: if (trAmt > 0) { 125: LENDING_POOL.PMTransfer( - 126: LENDING_POOL.getReserveData(address(tokenisedTicker[step])).aTokenAddress, + 126: LENDING_POOL.getReserveData(address(stepTicker_)).aTokenAddress, 127: msg.sender, 128: trAmt 129: ); - 130: uint256 ttAmt = LENDING_POOL.withdraw(address(tokenisedTicker[step]), type(uint256).max, address(this)); + 130: uint256 ttAmt = LENDING_POOL.withdraw(address(stepTicker_), type(uint256).max, address(this)); - 131: tokenisedTicker[step].withdraw(ttAmt, 0, 0); + 131: stepTicker_.withdraw(ttAmt, 0, 0); - 132: emit Withdraw(msg.sender, address(tokenisedTicker[step]), trAmt); + 132: emit Withdraw(msg.sender, address(stepTicker_), trAmt); 133: }
100 GAS, 1 SLOD
for each iterationsState variable should be cached inside the loop if it is accessed multiple times in the loop. This is because accessing a state variable from memory is a relatively expensive operation, so caching the variable in a local variable will improve performance
stepList[i]
should be cached : Saves 100 GAS, 1 SLOT
for every iterationsFILE: 2023-08-goodentry/contracts/RangeManager.sol 62: for (uint i = 0; i < len; i++) { + Step stepList_ = stepList[i]; - 63: if (start >= stepList[i].end || end <= stepList[i].start) { + 63: if (start >= stepList_.end || end <= stepList_.start) { 64: continue; 65: }
400 GAS
FAIL CHEEPLY INSTEAD OF COSTLY
Checks that involve constants should come before checks that involve state variables, function calls, and calculations. By doing these checks first, the function is able to revert before wasting a Gcoldsload (2100 gas) in a function that may ultimately revert in the unhappy case.
400 GAS
FILE: 2023-08-goodentry/contracts/GeVault.sol + 252: require(amount > 0 || msg.value > 0, "GEV: Deposit Zero"); 249: require(isEnabled, "GEV: Pool Disabled"); 250: require(poolMatchesOracle(), "GEV: Oracle Error"); 251: require(token == address(token0) || token == address(token1), "GEV: Invalid Token"); - 252: require(amount > 0 || msg.value > 0, "GEV: Deposit Zero");
view
or pure
keywords for functions that don't modify state to save gasThe view and pure keywords can be used to indicate that a function does not modify state. This can save gas, because the compiler will not need to check the gas costs of the function's internal operations
FILE: Breadcrumbs2023-08-goodentry/contracts/PositionManager/OptionsPositionManager.sol 533: function sanityCheckUnderlying(address tr, address token0, address token1) internal { 544: function addDust(address debtAsset, address token0, address token1) internal returns (uint amount){
#0 - c4-judge
2023-08-20T17:03:09Z
gzeon-c4 marked the issue as grade-a
#1 - c4-sponsor
2023-08-23T14:03:54Z
Keref marked the issue as sponsor confirmed
🌟 Selected for report: catellatech
Also found by: 0xSmartContract, K42, Sathish9098, digitizeworx
Read all documents with very fast forward way
Then read the Readme.md file understood following things
GoodEntry is a perpetual options trading platform, or protected perps: user can trade perps with limited downside. It is built on top of Uniswap v3 and relies on single tick liquidity
Then clone the repo and setup test environment to make sure all written tests are working correct
Then Jump into the codebase analysis. In first iteration just identified the important GAS and QA related findings.
calldata
instead of memory for read-only arguments in external functions saves gaspush()ed
but not pop()ed
contract-existence
checks before low-level callsrecipient
may consume all transaction gassafeApprove()
is deprecatedtreasuryFee
may cause problem in futurelatestRoundData
return stale or incorrect resultliquidation
This analysis report is meant to accompany my Gas Optimizations Report submission. All insights will be given through the perspective of optimizing the codebase
These optimizations, backed by opcode explanations, enhance the contract's gas efficiency and overall performance
The "onlyowner" keyword in smart contracts allows a single address to have complete control over the contract. This can lead to centralization risks, as the single owner could potentially abuse their power
The single owner could:
101: function setEnabled(bool _isEnabled) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L101-L101
108: function setTreasury(address newTreasury) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L108-L108
116: function pushTick(address tr) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L116-L116
137: function shiftTick(address tr) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L137-L137
167: function modifyTick(address tr, uint index) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L167-L167
183: function setBaseFee(uint newBaseFeeX4) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L183-L183
191: function setTvlCap(uint newTvlCap) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol#L191-L191
94: function emergencyWithdraw(ERC20 token) onlyOwner external
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/helper/V3Proxy.sol#L94-L94
75: function generateRange(uint128 startX10, uint128 endX10, string memory startName, string memory endName, address beacon) external onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/RangeManager.sol#L75-L75
95: function initRange(address tr, uint amount0, uint amount1) external onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/RangeManager.sol#L95-L95
48: function deprecatePool(uint poolId) public onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/RoeRouter.sol#L48-L48
59: function addPool( 60: address lendingPoolAddressProvider, 61: address token0, 62: address token1, 63: address ammRouter 64: ) 65: public onlyOwner 66: returns (uint poolId) 67:
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/RoeRouter.sol#L65-L65
24: function setHardcodedPrice(int256 _hardcodedPrice) external onlyOwner
https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/helper/FixedOracle.sol#L24-L24
10 hours
10 hours
#0 - c4-judge
2023-08-20T17:08:14Z
gzeon-c4 marked the issue as grade-a