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: 16/80
Findings: 2
Award: $678.68
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
17.345 USDC - $17.34
General Optimizations =
Possible Optimization 1 =
Here is the optimized code snippet:
function executeBuyOptions( uint poolId, address[] calldata assets, uint256[] calldata amounts, address user, address[] memory sourceSwap ) internal { (ILendingPool lendingPool,,, address token0, address token1 ) = getPoolAddresses(poolId); require( address(lendingPool) == msg.sender, "OPM: Call Unallowed"); // Optimization: Move the call to withdrawOptionAssets outside the loop withdrawOptionAssets(poolId, assets, amounts, sourceSwap, user); // send all tokens to lendingPool cleanup(lendingPool, user, token0); cleanup(lendingPool, user, token1); }
Possible Optimization 2 =
Here is the optimized code:
function executeLiquidation( uint poolId, address[] calldata assets, uint256[] calldata amounts, address user, address collateral ) internal { (ILendingPool lendingPool,,, address token0, address token1) = getPoolAddresses(poolId); require( address(lendingPool) == msg.sender, "OPM: Call Unallowed"); uint[2] memory amts = [ERC20(token0).balanceOf(address(this)), ERC20(token1).balanceOf(address(this))]; // Optimization: Move the call to closeDebt outside the loop uint debt = closeDebt(poolId, address(this), assets, amounts, collateral); for ( uint8 k =0; k<assets.length; k++){ address debtAsset = assets[k]; // simple liquidation: debt is transferred from user to liquidator and collateral deposited to roe uint amount = amounts[k]; // liquidate and send assets here checkSetAllowance(debtAsset, address(lendingPool), amount); lendingPool.liquidationCall(collateral, debtAsset, user, amount, false); // repay tokens uint amt0 = ERC20(token0).balanceOf(address(this)); uint amt1 = ERC20(token1).balanceOf(address(this)); emit LiquidatePosition(user, debtAsset, debt, amt0 - amts[0], amt1 - amts[1]); amts[0] = amt0; amts[1] = amt1; } }
General Optimizations =
Possible Optimization 1 =
After Optimization:
function deposit(address token, uint amount) public payable nonReentrant returns (uint liquidity) { // Add Optimization here: ERC20 _token0 = token0; ERC20 _token1 = token1; bool _isEnabled = isEnabled; address _treasury = treasury; uint _tvlCap = tvlCap; require(_isEnabled, "GEV: Pool Disabled"); require(poolMatchesOracle(), "GEV: Oracle Error"); require(token == address(_token0) || token == address(_token1), "GEV: Invalid Token"); require(amount > 0 || msg.value > 0, "GEV: Deposit Zero"); // Wrap if necessary and deposit here if (msg.value > 0){ require(token == address(WETH), "GEV: Invalid Weth"); // wraps ETH by sending to the wrapper that sends back WETH WETH.deposit{value: msg.value}(); amount = msg.value; } else { ERC20(token).safeTransferFrom(msg.sender, address(this), amount); } // Send deposit fee to treasury uint fee = amount * getAdjustedBaseFee(token == address(_token0)) / 1e4; ERC20(token).safeTransfer(_treasury, fee); uint valueX8 = oracle.getAssetPrice(token) * (amount - fee) / 10**ERC20(token).decimals(); require(_tvlCap > valueX8 + getTVL(), "GEV: Max Cap Reached"); uint vaultValueX8 = getTVL(); uint tSupply = totalSupply(); // initial liquidity at 1e18 token ~ $1 if (tSupply == 0 || vaultValueX8 == 0) liquidity = valueX8 * 1e10; else { liquidity = tSupply * valueX8 / vaultValueX8; } rebalance(); require(liquidity > 0, "GEV: No Liquidity Added"); _mint(msg.sender, liquidity); emit Deposit(msg.sender, token, amount, liquidity); }
Possible Optimization 2 =
After Optimization:
function depositAndStash(TokenisableRange t, uint amount0, uint amount1) internal returns (uint liquidity){ if ( ERC20(address(token0)).allowance(address(this), address(t)) < amount0 ) { ERC20(address(token0)).safeIncreaseAllowance(address(t), amount0); } if ( ERC20(address(token1)).allowance(address(this), address(t)) < amount1 ) { ERC20(address(token1)).safeIncreaseAllowance(address(t), amount1); } liquidity = t.deposit(amount0, amount1); uint bal = t.balanceOf(address(this)); if (bal > 0){ if ( ERC20(address(t)).allowance(address(this), address(lendingPool)) < bal ) { ERC20(address(t)).safeIncreaseAllowance(address(lendingPool), bal); } lendingPool.deposit(address(t), bal, address(this), 0); } }
General Optimizations =
Possible Optimization 1 =
After Optimization:
function deposit(uint256 n0, uint256 n1) external nonReentrant returns (uint256 lpAmt) { // Once all assets were withdrawn after initialisation, this is considered closed // Prevents TR oracle values from being too manipulatable by emptying the range and redepositing require(totalSupply() > 0, "TR Closed"); claimFee(); TOKEN0.token.transferFrom(msg.sender, address(this), n0); TOKEN1.token.transferFrom(msg.sender, address(this), n1); uint newFee0; uint newFee1; // Calculate proportion of deposit that goes to pending fee pool, useful to deposit exact amount of liquidity and fully repay a position // Cannot repay only one side, if fees are both 0, or if one side is missing, skip adding fees here // if ( fee0+fee1 == 0 || (n0 == 0 && fee0 > 0) || (n1 == 0 && fee1 > 0) ) skip // DeMorgan: !( (n0 == 0 && fee0 > 0) || (n1 == 0 && fee1 > 0) ) = !(n0 == 0 && fee0 > 0) && !(n0 == 0 && fee1 > 0) if ( fee0+fee1 > 0 && ( n0 > 0 || fee0 == 0) && ( n1 > 0 || fee1 == 0 ) ){ address pool = V3_FACTORY.getPool(address(TOKEN0.token), address(TOKEN1.token), feeTier * 100); (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); (uint256 token0Amount, uint256 token1Amount) = LiquidityAmounts.getAmountsForLiquidity( sqrtPriceX96, TickMath.getSqrtRatioAtTick(lowerTick), TickMath.getSqrtRatioAtTick(upperTick), liquidity); if (token0Amount + fee0 > 0) newFee0 = n0 * fee0 / (token0Amount + fee0); if (token1Amount + fee1 > 0) newFee1 = n1 * fee1 / (token1Amount + fee1); fee0 += newFee0; fee1 += newFee1; n0 -= newFee0; n1 -= newFee1; } ERC20 _token0 = TOKEN0.token; ERC20 _token1 = TOKEN1.token; _token0.safeIncreaseAllowance(address(POS_MGR), n0); _token1.safeIncreaseAllowance(address(POS_MGR), n1); // New liquidity is indeed the amount of liquidity added, not the total, despite being unclear in Uniswap doc (uint128 newLiquidity, uint256 added0, uint256 added1) = POS_MGR.increaseLiquidity( INonfungiblePositionManager.IncreaseLiquidityParams({ tokenId: tokenId, amount0Desired: n0, amount1Desired: n1, amount0Min: n0 * 95 / 100, amount1Min: n1 * 95 / 100, deadline: block.timestamp }) ); uint256 feeLiquidity; if ( newFee0 == 0 && newFee1 == 0 ){ uint256 TOKEN0_PRICE = ORACLE.getAssetPrice(address(TOKEN0.token)); uint256 TOKEN1_PRICE = ORACLE.getAssetPrice(address(TOKEN1.token)); require (TOKEN0_PRICE > 0 && TOKEN1_PRICE > 0, "Invalid Oracle Price"); feeLiquidity = newLiquidity * ( (fee0 * TOKEN0_PRICE / 10 ** TOKEN0.decimals) + (fee1 * TOKEN1_PRICE / 10 ** TOKEN1.decimals) ) / ( (added0 * TOKEN0_PRICE / 10 ** TOKEN0.decimals) + (added1 * TOKEN1_PRICE / 10 ** TOKEN1.decimals) ); } lpAmt = totalSupply() * newLiquidity / (liquidity + feeLiquidity); liquidity = liquidity + newLiquidity; _mint(msg.sender, lpAmt); _token0.safeTransfer( msg.sender, n0 - added0); _token1.safeTransfer( msg.sender, n1 - added1); emit Deposit(msg.sender, lpAmt); }
Possible Optimization 2 =
After Optimization:
function claimFee() public { (uint256 newFee0, uint256 newFee1) = POS_MGR.collect( INonfungiblePositionManager.CollectParams({ tokenId: tokenId, recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max }) ); // If there's no new fees generated, skip compounding logic; if ((newFee0 == 0) && (newFee1 == 0)) return; uint tf0 = newFee0 * treasuryFee / 100; uint tf1 = newFee1 * treasuryFee / 100; if (tf0 > 0) TOKEN0.token.safeTransfer(treasury, tf0); if (tf1 > 0) TOKEN1.token.safeTransfer(treasury, tf1); uint _fee0 = fee0 + newFee0 - tf0; uint _fee1 = fee1 + newFee1 - tf1; // Calculate expected balance, (uint256 bal0, uint256 bal1) = returnExpectedBalanceWithoutFees(0, 0); // If accumulated more than 1% worth of fees, compound by adding fees to Uniswap position if ((_fee0 * 100 > bal0 ) && (_fee1 * 100 > bal1)) { TOKEN0.token.safeIncreaseAllowance(address(POS_MGR), _fee0); TOKEN1.token.safeIncreaseAllowance(address(POS_MGR), _fee1); (uint128 newLiquidity, uint256 added0, uint256 added1) = POS_MGR.increaseLiquidity( INonfungiblePositionManager.IncreaseLiquidityParams({ tokenId: tokenId, amount0Desired: _fee0, amount1Desired: _fee1, amount0Min: 0, amount1Min: 0, deadline: block.timestamp }) ); // check slippage: validate against value since token amounts can move widely uint token0Price = ORACLE.getAssetPrice(address(TOKEN0.token)); uint token1Price = ORACLE.getAssetPrice(address(TOKEN1.token)); uint addedValue = added0 * token0Price / 10**TOKEN0.decimals + added1 * token1Price / 10**TOKEN1.decimals; uint totalValue = bal0 * token0Price / 10**TOKEN0.decimals + bal1 * token1Price / 10**TOKEN1.decimals; uint liquidityValue = totalValue * newLiquidity / liquidity; require(addedValue > liquidityValue * 95 / 100 && liquidityValue > addedValue * 95 / 100, "TR: Claim Fee Slippage"); _fee0 -= added0; _fee1 -= added1; liquidity = liquidity + newLiquidity; } fee0 = _fee0; fee1 = _fee1; emit ClaimFees(newFee0, newFee1); }
SSTORE
operations, which cost 5000 gas each. If the allowances are already high enough, we could save up to 10,000 gas per transaction.Possible Optimization 1 =
safeApprove()
twice: once before the swap
to set the allowance, and once after the swap
to set the allowance back to zero. This is unnecessary because the Uniswap
router only uses the exact amount of tokens it needs for the swap
, and does not drain the entire allowance. Therefore, the second safeApprove()
call can be removed.For swapExactTokensForTokens():
function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts) { require(path.length == 2, "Direct swap only"); ERC20 ogInAsset = ERC20(path[0]); ogInAsset.safeTransferFrom(msg.sender, address(this), amountIn); ogInAsset.safeApprove(address(ROUTER), amountIn); amounts = new uint[](2); amounts[0] = amountIn; amounts[1] = ROUTER.exactInputSingle(ISwapRouter.ExactInputSingleParams(path[0], path[1], feeTier, msg.sender, deadline, amountIn, amountOutMin, 0)); emit Swap(msg.sender, path[0], path[1], amounts[0], amounts[1]); }
You can do the same in swapExactTokensForTokens(), swapTokensForExactTokens(), swapExactETHForTokens(), swapETHForExactTokens(), swapTokensForExactETH(), swapExactTokensForETH() also to save a significant amount of gas.
SSTORE
operations, which cost 5000 gas each. So, we could save up to 5000 gas per transaction.Possible Optimization 2 =
Uniswap
router send the ETH
directly to the user.After Optimization:
function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) payable external returns (uint[] memory amounts) { require(path.length == 2, "Direct swap only"); require(path[1] == ROUTER.WETH9(), "Invalid path"); ERC20 ogInAsset = ERC20(path[0]); ogInAsset.safeTransferFrom(msg.sender, address(this), amountInMax); ogInAsset.safeApprove(address(ROUTER), amountInMax); amounts = new uint[](2); amounts[0] = ROUTER.exactOutputSingle(ISwapRouter.ExactOutputSingleParams(path[0], path[1], feeTier, msg.sender, deadline, amountOut, amountInMax, 0)); amounts[1] = amountOut; emit Swap(msg.sender, path[0], path[1], amounts[0], amounts[1]); }
SSTORE
operations, which cost 5000 gas each. So, we could save up to 5700 gas per transaction.The same optimization can be applied to swapExactTokensForETH() as well.
Possible Optimization 1 =
ASSET_0
and ASSET_1
is greater than zero before approving and depositing.After Optimization:
function cleanup() internal { uint256 asset0_amt = ASSET_0.balanceOf(address(this)); uint256 asset1_amt = ASSET_1.balanceOf(address(this)); if (asset0_amt > 0) { ASSET_0.safeIncreaseAllowance(address(LENDING_POOL), asset0_amt); LENDING_POOL.deposit(address(ASSET_0), asset0_amt, msg.sender, 0); } if (asset1_amt > 0) { ASSET_1.safeIncreaseAllowance(address(LENDING_POOL), asset1_amt); LENDING_POOL.deposit(address(ASSET_1), asset1_amt, msg.sender, 0); } (,,,,,uint256 hf) = LENDING_POOL.getUserAccountData(msg.sender); require(hf > 1.01e18, "Health factor is too low"); }
SSTORE
operations, which cost 5000 gas each, and CALL
operations, which cost 700 gas each. So, we could save up to 5700 gas per transaction when the balance is zero.Possible Optimization 2 =
LENDING_POOL
for each tokenised range and tokenised ticker is greater than zero before transferring, withdrawing, and emitting the event.After Optimization:
function removeFromStep(uint256 step) internal { require(step < tokenisedRanges.length && step < tokenisedTicker.length, "Invalid step"); uint256 trAmt; trAmt = ERC20(LENDING_POOL.getReserveData(address(tokenisedRanges[step])).aTokenAddress).balanceOf(msg.sender); if (trAmt > 0) { LENDING_POOL.PMTransfer( LENDING_POOL.getReserveData(address(tokenisedRanges[step])).aTokenAddress, msg.sender, trAmt ); trAmt = LENDING_POOL.withdraw(address(tokenisedRanges[step]), type(uint256).max, address(this)); tokenisedRanges[step].withdraw(trAmt, 0, 0); emit Withdraw(msg.sender, address(tokenisedRanges[step]), trAmt); } trAmt = ERC20(LENDING_POOL.getReserveData(address(tokenisedTicker[step])).aTokenAddress).balanceOf(msg.sender); if (trAmt > 0) { LENDING_POOL.PMTransfer( LENDING_POOL.getReserveData(address(tokenisedTicker[step])).aTokenAddress, msg.sender, trAmt ); uint256 ttAmt = LENDING_POOL.withdraw(address(tokenisedTicker[step]), type(uint256).max, address(this)); tokenisedTicker[step].withdraw(ttAmt, 0, 0); emit Withdraw(msg.sender, address(tokenisedTicker[step]), trAmt); } }
SSTORE
operations, which cost 5000 gas each, and CALL
operations, which cost 700 gas each. So, we could save up to 5700 gas per transaction when the balance is zero.Possible Optimization 1 =
After Optimization:
function cleanup(ILendingPool LP, address user, address asset) internal { uint amt = ERC20(asset).balanceOf(address(this)); if (amt > 0) { checkSetAllowance(asset, address(LP), amt); // if there is a debt, try to repay the debt uint debt = ERC20(LP.getReserveData(asset).variableDebtTokenAddress).balanceOf(user); if ( debt > 0 ){ if (amt <= debt ) { LP.repay( asset, amt, 2, user); return; } else { LP.repay( asset, debt, 2, user); amt = amt - debt; } } // deposit remaining tokens if (amt > 0) { LP.deposit( asset, amt, user, 0 ); } } }
SSTORE
operations, which cost 5000 gas each, and CALL
operations, which cost 700 gas each. So, we could save up to 5700 gas per transaction when the balance is zero.Possible Optimization 2 =
After Optimization:
function PMWithdraw(ILendingPool LP, address user, address asset, uint amount) internal { if ( amount > 0 && ERC20(LP.getReserveData(asset).aTokenAddress).balanceOf(user) > 0){ LP.PMTransfer(LP.getReserveData(asset).aTokenAddress, user, amount); LP.withdraw(asset, amount, address(this)); } }
SSTORE
operations, which cost 5000 gas each, and CALL
operations, which cost 700 gas each. So, we could save up to 5700 gas per transaction when the balance is zero.#0 - c4-pre-sort
2023-08-10T17:12:32Z
141345 marked the issue as high quality report
#1 - c4-sponsor
2023-08-15T06:00:16Z
Keref marked the issue as sponsor disputed
#2 - c4-sponsor
2023-08-15T06:03:18Z
Keref marked the issue as sponsor acknowledged
#3 - Keref
2023-08-15T06:03:50Z
Valid ones for TokenisableRange.sol Rest of it is trying to break code factorization to save 1 internal call, and at worst the last example makes a 2nd useless check (amt > 0) and actually increases gas usage.
#4 - c4-judge
2023-08-20T17:03:39Z
gzeon-c4 marked the issue as grade-b
🌟 Selected for report: catellatech
Also found by: 0xSmartContract, K42, Sathish9098, digitizeworx
The tokenomics model is structured as follows:
Initial Farm Offering (IFO): 15% of the tokens are allocated during the public launch via an IFO. The proceeds from this offering contribute to Protocol Owned Liquidity, with fees supporting protocol development. The token launch date will be announced first on the GoodEntry Discord server.
Pre-Mining Program: 5% of the tokens are allocated to a pre-mining program, which will be distributed linearly over six months. Details of the program mechanics will be released first on the GoodEntry Discord server.
Liquidity Mining: 25% of the tokens are dedicated to liquidity mining over three years. This is designed to incentivize users to provide liquidity to the platform.
Partnerships: 15% of the tokens are allocated for partnerships. This allocation could be used to form strategic alliances with other platforms or entities.
Seasonal Allocation: 5% of the tokens are designated for Season 1, with a 3-month cliff followed by a 9-month linear lockup. Future seasons will see pre-mined tokens sent to a multisig wallet, released on a seasonal basis.
Reserves: 18% of the tokens are held in reserves, pre-minted to a multisig wallet. These reserves can be used for various functions to support the growth and stability of the platform.
Team Allocation: 22% of the tokens are assigned to the team, subject to a 1-year cliff and a 3-year linear unlock. This ensures that the team is incentivized to continue developing and improving the platform over the long term.
The GoodEntry ecosystem is thus designed to be both inclusive and sustainable, with a clear focus on community collaboration and long-term growth. The tokenomics model plays a crucial role in this, providing the necessary incentives for users to participate in the ecosystem and contribute to its development.
The platform introduces a unique tokenomics model, with two tokens: $GE
and $GEP
. $GE
is the governance token, used for voting on proposals and earning fees from the platform. $GEP
is the protocol token, used for staking, liquidity mining, and earning rewards.
The architecture of the platform is sound, with a clear separation of concerns and modular design. However, there are a few areas where improvements could be made:
$GE
governance token, which allows token holders to vote on proposals and influence the direction of the platform.The main contracts in the GoodEntry platform are:
Tokenisable
Ranges.TokenisableRanges
, and helping user enter and exit these ranges through the Lending Pool.20 hours
#0 - c4-judge
2023-08-20T17:08:10Z
gzeon-c4 marked the issue as grade-a