Platform: Code4rena
Start Date: 21/11/2022
Pot Size: $90,500 USDC
Total HM: 18
Participants: 101
Period: 7 days
Judge: Picodes
Total Solo HM: 4
Id: 183
League: ETH
Rank: 53/101
Findings: 2
Award: $69.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: deliriusz
Also found by: 0x52, 0xLad, 0xbepresent, Englave, R2, Ruhum, cccz, gzeon, hihen, keccak123, ladboy233, pashov, pedroais, perseverancesuccess, rbserver, rvierdiiev, simon135, unforgiven, wagmi, xiaoming90
15.9293 USDC - $15.93
when the router is going to the trade from baseGmxReward
to Gmx
the slippage set is 1
which is saying we allow the trade to take 99% percent slippage
Also a similar thing with sqrtPriceLimitX96
amountOutMinimum: we are setting it to zero, but this is a significant risk in production. For a real deployment, this value should be calculated using our SDK or an on-chain price oracle - this helps protect against getting an unusually bad price for a trade due to a front-running sandwich or another type of price manipulation sqrtPriceLimitX96: We set this to zero - which makes this parameter inactive. In production, this value can be used to set the limit for the price the swap will push the pool to, which can help protect against price impact or for setting up logic in a variety of price-relevant mechanisms.
*/ function compound( uint24 fee, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96, bool optOutIncentive ) public returns ( uint256 gmxBaseRewardAmountIn, uint256 gmxAmountOut, uint256 pxGmxMintAmount, uint256 totalFee, uint256 incentive ) { if (fee == 0) revert InvalidParam(); if (amountOutMinimum == 0) revert InvalidParam(); uint256 assetsBeforeClaim = asset.balanceOf(address(this)); PirexRewards(rewardsModule).claim(asset, address(this)); // Swap the entire reward balance for GMX gmxBaseRewardAmountIn = gmxBaseReward.balanceOf(address(this)); if (gmxBaseRewardAmountIn != 0) { gmxAmountOut = SWAP_ROUTER.exactInputSingle( IV3SwapRouter.ExactInputSingleParams({ tokenIn: address(gmxBaseReward), tokenOut: address(gmx), fee: fee, recipient: address(this), amountIn: gmxBaseRewardAmountIn, amountOutMinimum: amountOutMinimum, sqrtPriceLimitX96: sqrtPriceLimitX96 }) );
As you can see slippage
and sqrtPrice
is user supplied but where most users call them the args are not user-supplied
function withdraw( uint256 assets, address receiver, address owner ) public override returns (uint256 shares) { // Compound rewards and ensure they are properly accounted for prior to withdrawal calculation compound(poolFee, 1, 0, true);
here the slippage is set to 1 and sqrtPrice is 0 which is bad because the user can be sandwiched and the contract will lose funds. There will be fewer funds to give out as rewards which can be bad for the protocol.
make it user-supplied or make it the same or close the same amount of what is withdrawn or deposited etc.
Also, do the same for sqrtPrice
which can also help get the most amount of funds in the contract
#0 - c4-judge
2022-12-03T18:04:04Z
Picodes marked the issue as duplicate of #296
#1 - c4-judge
2022-12-03T18:06:15Z
Picodes marked the issue as duplicate of #185
#2 - c4-judge
2023-01-01T11:21:04Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-01-01T11:37:55Z
Picodes changed the severity to 2 (Med Risk)
#4 - C4-Staff
2023-01-10T22:10:37Z
JeeberC4 marked the issue as duplicate of #137
🌟 Selected for report: 0xSmartContract
Also found by: 0xAgro, 0xNazgul, 0xPanda, 0xbepresent, 0xfuje, Awesome, B2, Bnke0x0, Deivitto, Diana, Funen, Jeiwan, JohnSmith, Josiah, R2, RaymondFam, Rolezn, Sathish9098, Waze, adriro, aphak5010, brgltd, btk, carrotsmuggler, ch0bu, chaduke, codeislight, codexploder, cryptostellar5, csanuragjain, danyams, datapunk, delfin454000, deliriusz, eierina, erictee, fatherOfBlocks, gz627, gzeon, hansfriese, hihen, jadezti, joestakey, keccak123, martin, nameruse, oyc_109, pedr02b2, perseverancesuccess, rbserver, rotcivegaf, rvierdiiev, sakshamguruji, shark, simon135, subtle77, unforgiven, xiaoming90, yixxas
53.4851 USDC - $53.49
if (_pxGmx == address(0)) revert ZeroAddress(); if (_pxGlp == address(0)) revert ZeroAddress(); if (_pirexFees == address(0)) revert ZeroAddress(); if (_pirexRewards == address(0)) revert ZeroAddress(); if (_delegateRegistry == address(0)) revert ZeroAddress(); if (_gmxBaseReward == address(0)) revert ZeroAddress(); if (_gmx == address(0)) revert ZeroAddress(); if (_esGmx == address(0)) revert ZeroAddress(); if (_gmxRewardRouterV2 == address(0)) revert ZeroAddress(); if (_stakedGlp == address(0)) revert ZeroAddress();
instead, make it into a modifier
modifer nonzero(adddress t) { if(t== address(0)) revert ZeroAddress(); }
assets
var to amount
for readabilityfunction _computeAssetAmounts(Fees f, uint256 assets)
make into uint256 amount
:219L: PirexGmx.sol
postFeeAmount
and feeAmount
it's very confusing because the postFeeAmount
is the real fee amount and feeAmount
is just the fee subtracted
instead:
feeAmount
= the fee
postFeeAmount
the amount-fee
beforeDeposit
to stop huge slippage loss and to make it more useableif (totalAssets() != 0) beforeDeposit(address(0), 0, 0);
distrbutorbalance= 0
then rewards won't be processed even though there are rewards that need to get processeduint256 blockReward = pendingRewards > distributorBalance ? distributorBalance : pendingRewards; uint256 precision = r.PRECISION(); uint256 cumulativeRewardPerToken = r.cumulativeRewardPerToken() + ((blockReward * precision) / r.totalSupply());
so if pendingRewards > distributorBalance
The amount is distributorBalance
instead of the actual pending amounts
ex:
pendingRewards= 1ether
distributorBalance=0
The rewards will be 0
and no rewards will be distributed because of the ternary operator wrong use
instead, make the calculation into
blockReward=pendingRewards; uint256 cumulativeRewardPerToken = r.cumulativeRewardPerToken() + ((blockReward * precision) / r.totalSupply());
function distributeFees(ERC20 token) external { uint256 distribution = token.balanceOf(address(this)); uint256 treasuryDistribution = (distribution * treasuryFeePercent) / FEE_PERCENT_DENOMINATOR; uint256 contributorsDistribution = distribution - treasuryDistribution;
if distribution
is less than FEE_PERCENT_DENOMINATOR
then it will be zero and be a waste of gas by the caller
add a zero check to make sure it's not zero.
globalState.lastSupply = totalSupply.safeCastTo224();
totalPxGlpFee
is less than the Incentive
which is possible with the calculationsuint256 newAssets = totalAssets() - preClaimTotalAssets; if (newAssets != 0) { totalPxGlpFee = (newAssets * platformFee) / FEE_DENOMINATOR; pxGlpIncentive = optOutIncentive ? 0 : (totalPxGlpFee * compoundIncentive) / FEE_DENOMINATOR; if (pxGlpIncentive != 0) asset.safeTransfer(msg.sender, pxGlpIncentive); asset.safeTransfer(owner, totalPxGlpFee - pxGlpIncentive); }
gmxBaseReward.safeApprove(address(SWAP_ROUTER), type(uint256).max); gmx.safeApprove(_platform, type(uint256).max);
uint256 rewards = u.rewards + u.lastBalance * (block.timestamp - u.lastUpdate); u.lastUpdate = block.timestamp.safeCastTo32(); u.lastBalance = balance.safeCastTo224(); u.rewards = rewards;
ex:
rewardState= 1e18
userRewards=100
globalRewards=10
(1e18 *100) / 10= 1e19
it can really not work when the tokens are going through a loop and the amounts get bigger causing a revert at the end of the loop and making the function revert and rewards won't work.
uint256 rewardState = p.rewardStates[rewardToken]; uint256 amount = (rewardState * userRewards) / globalRewards; if (amount != 0) { // Update reward state (i.e. amount) to reflect reward tokens transferred out p.rewardStates[rewardToken] = rewardState - amount;
https://github.com/code-423n4/2022-11-redactedcartel/blob/684627b7889e34ba7799e50074d138361f0f532b/src/PirexRewards.sol#L418
make sure the calculation is good and make a limit for how many rewardTokens
you can loop through
#0 - c4-judge
2022-12-05T09:46:59Z
Picodes marked the issue as grade-b