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: 9/101
Findings: 7
Award: $2,188.94
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: KingNFT
Also found by: 0x52, HE1M, ladboy233, rvierdiiev, xiaoming90
902.6222 USDC - $902.62
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/PirexGmx.sol#L651-L662
Cooldown period of GlpManager that is 15 minutes now can restrict users from withdrawing their assets
Gmx GlpManager has cooldown period(15 minutes currently) that means that after you staked you can't unstake for some amount of time. https://arbiscan.io/address/0x321F653eED006AD1C29D174e17d96351BDe22649#code#L938
require(lastAddedAt[_account].add(cooldownDuration) <= block.timestamp, "GlpManager: cooldown duration not yet passed");
When users deposit directly through the PirexGmx or using AutoPxGlp or AutoPxGmx then their assets are transferred to PirexGmx and then this contract stakes to gmx system. That means that every time any user wants to deposit some assets, then PirexGmx will not be able to unstake during cooldown period. Note that max cooldown period is 48 hours. Then when someone want to withdraw it should burn his pxGmx tokens through PirexGmx which will call unstaling on gmx system. If cooldown period is not passed yet then users will not be able to withdraw their assets.
VsCode
You can use pausing mechanism to make contract nor accept deposits when paused and allow people to withdraw when paused. That means that in case when people can't withdraw, because a lot of deposits you call pause and then people can't deposit. After cooldown period another people can withdraw, but you should remove whenNotPaused modifier from redeem functions.
#0 - c4-judge
2022-12-04T00:24:28Z
Picodes marked the issue as duplicate of #110
#1 - c4-judge
2022-12-05T10:46:09Z
Picodes marked the issue as duplicate of #113
#2 - c4-judge
2023-01-01T11:08:37Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-01-01T11:38:10Z
Picodes changed the severity to 3 (High Risk)
🌟 Selected for report: xiaoming90
Also found by: 0xSmartContract, 8olidity, PaludoX0, Ruhum, adriro, bin2chen, cccz, koxuan, ladboy233, pashov, poirots, rvierdiiev, unforgiven
166.5211 USDC - $166.52
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L177-L195
AutoPxGlp.previewWithdraw doesn't round up shares count and allows to burn less shares than needed using withdraw function.
Per EIP 4626's Security Considerations (https://eips.ethereum.org/EIPS/eip-4626) previewWithdraw function should round up. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L177-L195
function previewWithdraw(uint256 assets) public view override returns (uint256) { // Calculate shares based on the specified assets' proportion of the pool uint256 shares = convertToShares(assets); // Save 1 SLOAD uint256 _totalSupply = totalSupply; // Factor in additional shares to fulfill withdrawal if user is not the last to withdraw return (_totalSupply == 0 || _totalSupply - shares == 0) ? shares : (shares * FEE_DENOMINATOR) / (FEE_DENOMINATOR - withdrawalPenalty); }
To calculate shares amount AutoPxGlp.previewWithdraw function uses convertToShares function which calculates shares with rounding down. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/PirexERC4626.sol#L156-L165
function convertToShares(uint256 assets) public view virtual returns (uint256) { uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets()); }
Later there is no any rounding up as you can see. Because of that in case when assets amount that user wants to withdraw is not equal to integer value of shares, then less amount of shares will be burnt and as result protocol will have loses.
The same thing exists in AutoPxGmx.previewWithdraw.
Also there is another thing that attacker can do here. If he call withdraw with small assets count such that shares amount will be < 1, then it will be rounded to 0 and in this case attacker don't need to have allowance(as he withdraw 0 shares) and he can take such small amounts from any account. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/PirexERC4626.sol#L99-L122
function withdraw( uint256 assets, address receiver, address owner ) public virtual returns (uint256 shares) { shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. if (msg.sender != owner) { uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; } beforeWithdraw(owner, assets, shares); _burn(owner, shares); emit Withdraw(msg.sender, receiver, owner, assets, shares); asset.safeTransfer(receiver, assets); afterWithdraw(owner, assets, shares); }
VsCode
When calculate shares to burn, you need to round that amount up.
#0 - c4-judge
2022-12-04T13:49:42Z
Picodes marked the issue as duplicate of #264
#1 - c4-judge
2023-01-01T11:06:51Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-01-01T11:33:52Z
Picodes changed the severity to 3 (High Risk)
#3 - C4-Staff
2023-01-10T21:57:30Z
JeeberC4 marked the issue as duplicate of #264
#4 - liveactionllama
2023-01-11T18:44:22Z
Duplicate of #178
🌟 Selected for report: Jeiwan
Also found by: 0xLad, 0xSmartContract, 8olidity, HE1M, JohnSmith, KingNFT, Koolex, Lambda, R2, __141345__, carrotsmuggler, cccz, gogo, hl_, joestakey, koxuan, ladboy233, pashov, peanuts, rbserver, rvierdiiev, seyni, unforgiven, xiaoming90, yongskiws
12.6621 USDC - $12.66
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L304-L356 https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/PirexGmx.sol#L422-L465
Share price manipulation is possible for the first depositor of AutoPxGlp by depositing small amount first to AutoPxGlp and then transferring some pxGMX tokens to AutoPxGlp directly.
When users deposit into AutoPxGlp, new pxGMX tokens are minted for the AutoPxGlp. To know how many assets are controlled by AutoPxGlp it uses totalAssets function. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L142-L144
function totalAssets() public view override returns (uint256) { return asset.balanceOf(address(this)); }
When user provides tokens he wants to deposit they are sent to PirexGmx which then mints some amount of pxGMX tokens for AutoPxGlp. Later using that amount of minted tokens and totalAssets amount the amount of shares in AutoPxGlp is calculated. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L304-L322
function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. uint256 supply = totalSupply; if ( (shares = supply == 0 ? assets : assets.mulDivDown(supply, totalAssets() - assets)) == 0 ) revert ZeroShares(); _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); afterDeposit(receiver, assets, shares); }
Also you should note that it's possible for anyone to mint pxGMX tokens directly through the PirexGmx contract to have some pxGMX. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/PirexGmx.sol#L422-L465
function depositFsGlp(uint256 amount, address receiver) external whenNotPaused nonReentrant returns ( uint256, uint256, uint256 ) { if (amount == 0) revert ZeroAmount(); if (receiver == address(0)) revert ZeroAddress(); // Transfer the caller's fsGLP (unstaked for the user, staked for this contract) stakedGlp.transferFrom(msg.sender, address(this), amount); // Get the pxGLP amounts for the receiver and the protocol (fees) (uint256 postFeeAmount, uint256 feeAmount) = _computeAssetAmounts( Fees.Deposit, amount ); // Mint pxGLP for the receiver (excludes fees) pxGlp.mint(receiver, postFeeAmount); // Mint pxGLP for fee distribution contract if (feeAmount != 0) { pxGlp.mint(address(pirexFees), feeAmount); } emit DepositGlp( msg.sender, receiver, address(stakedGlp), 0, 0, 0, amount, postFeeAmount, feeAmount ); return (amount, postFeeAmount, feeAmount); }
This all allows first depositor to manipulate with share price. 1.Attacker deposit through PirexGmx.depositFsGlp(or any similar function) and mints huge amount of pxGMX tokens. 2.Attacker deposit(as first depositor) through AutoPxGlp.depositFsGlp(or any similar function) with minimum amount. 3.Attacker transfer his big amount of pxGMX tokens to AutoPxGlp. 4.Now because of big amount of totalAssets and small amount of totalSupply all next depositors will lose some tokens because of rounding while attacker will benefit as he will receive more tokens.
VsCode
Restrict first depositor of AutoPxGlp to deposit big amount of assets to mint big amount of shares on start.
#0 - c4-judge
2022-12-03T21:07:11Z
Picodes marked the issue as duplicate of #407
#1 - c4-judge
2022-12-03T21:07:16Z
Picodes marked the issue as partial-50
#2 - Picodes
2022-12-03T21:07:34Z
The mitigation is incorrect, and the root cause of the issue is not really identified
#3 - C4-Staff
2023-01-10T21:54:30Z
JeeberC4 marked the issue as duplicate of #275
🌟 Selected for report: rvierdiiev
965.7686 USDC - $965.77
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/PirexGmx.sol#L921-L935
PirexGmx.initiateMigration can be blocked so contract will not be able to migrate his funds to another contract using gmx.
PirexGmx was designed with the thought that the current contract can be changed with another during migration. PirexGmx.initiateMigration is the first point in this long process. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/PirexGmx.sol#L921-L935
function initiateMigration(address newContract) external whenPaused onlyOwner { if (newContract == address(0)) revert ZeroAddress(); // Notify the reward router that the current/old contract is going to perform // full account transfer to the specified new contract gmxRewardRouterV2.signalTransfer(newContract); migratedTo = newContract; emit InitiateMigration(newContract); }
As you can see gmxRewardRouterV2.signalTransfer(newContract);
is called to start migration.
This is the code of signalTransfer function
https://arbiscan.io/address/0xA906F338CB21815cBc4Bc87ace9e68c87eF8d8F1#code#F1#L282
function signalTransfer(address _receiver) external nonReentrant { require(IERC20(gmxVester).balanceOf(msg.sender) == 0, "RewardRouter: sender has vested tokens"); require(IERC20(glpVester).balanceOf(msg.sender) == 0, "RewardRouter: sender has vested tokens"); _validateReceiver(_receiver); pendingReceivers[msg.sender] = _receiver; }
As you can see the main condition to start migration is that PirexGmx doesn't control any gmxVester and glpVester tokens. So attacker can deposit and receive such tokens and then just transfer tokens directly to PirexGmx. As a result migration will never be possible as there is no possibility for PirexGmx to burn those gmxVester and glpVester tokens.
Also in same way migration receiver can also be blocked.
VsCode
Think about how to make contract to be sure that he doesn't control any gmxVester and glpVester tokens before migration.
#0 - c4-judge
2022-12-04T00:06:26Z
Picodes marked the issue as duplicate of #198
#1 - c4-judge
2022-12-30T20:33:52Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2022-12-30T20:33:56Z
Picodes marked the issue as selected for report
#3 - c4-judge
2023-01-01T11:39:06Z
Picodes changed the severity to 2 (Med Risk)
#4 - C4-Staff
2023-01-10T22:00:02Z
JeeberC4 marked the issue as not a duplicate
#5 - C4-Staff
2023-01-10T22:00:16Z
JeeberC4 marked the issue as primary issue
#6 - kphed
2023-01-13T15:37:51Z
This issue is invalid and not possible to carry out as a non-GMX insider (if the GMX team and multisig were malicious, there would be many other ways in which they can steal value, so this specific vector would be the least of our concerns) for the following reasons:
Vester.sol | Lines 246-263
// empty implementation, tokens are non-transferrable function transfer(address /* recipient */, uint256 /* amount */) public override returns (bool) { revert("Vester: non-transferrable"); } ... // empty implementation, tokens are non-transferrable function transferFrom(address /* sender */, address /* recipient */, uint256 /* amount */) public virtual override returns (bool) { revert("Vester: non-transferrable"); }
depositForAccount
method can only be called by an account set by the GMX team as a "handler" so an attacker can't volunteer esGMX be vested on behalf of another account. Even if depositForAccount
were to be callable by anyone, esGMX has to first be unstaked before it can be deposited for vesting, which is never the case for our contracts.🌟 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
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L242-L313
AutoPxGmx.compound can be frontrunned and because no slippage is provided it's possible that the price for swap will be very bad and received amount will be small.
AutoPxGmx.compound function is called very often in the AutoPxGmx contract to update rewards that are earned by users.
And all time the function is called like this compound(poolFee, 1, 0, true);
. In such scenario the minimum swap amount is only 1.
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L242-L313
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 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 }) ); // Deposit entire GMX balance for pxGMX, increasing the asset/share amount (, pxGmxMintAmount, ) = PirexGmx(platform).depositGmx( gmx.balanceOf(address(this)), address(this) ); } // Only distribute fees if the amount of vault assets increased if ((totalAssets() - assetsBeforeClaim) != 0) { totalFee = ((asset.balanceOf(address(this)) - assetsBeforeClaim) * platformFee) / FEE_DENOMINATOR; incentive = optOutIncentive ? 0 : (totalFee * compoundIncentive) / FEE_DENOMINATOR; if (incentive != 0) asset.safeTransfer(msg.sender, incentive); asset.safeTransfer(owner, totalFee - incentive); } emit Compounded( msg.sender, fee, amountOutMinimum, sqrtPriceLimitX96, gmxBaseRewardAmountIn, gmxAmountOut, pxGmxMintAmount, totalFee, incentive ); }
Later function use that amountOutMinimum == 1 param to make swap with uniswap. Also sqrtPriceLimitX96 param is set to 0 which also switch off price control.
That means that in case when price suddenly changes the swap can still be executed and gmx tokens will be swapped for a very small amount.
Also because the function compound is public, everyone can call it. That means that someone can call the function right after he changed the price in the pool to make protocol receive less rewards.
Also the same problem has AutoPxGlp contract.
VsCode
Add some slippage protection, for example you can calculate how much you expect to receive and then make slippage 10%.
#0 - c4-judge
2022-12-04T13:28:28Z
Picodes marked the issue as duplicate of #185
#1 - c4-judge
2023-01-01T11:07:46Z
Picodes marked the issue as satisfactory
#2 - C4-Staff
2023-01-10T22:10:37Z
JeeberC4 marked the issue as duplicate of #137
🌟 Selected for report: xiaoming90
Also found by: 0x52, 8olidity, aphak5010, bin2chen, cccz, hansfriese, hihen, joestakey, ladboy233, rvierdiiev, unforgiven
71.9536 USDC - $71.95
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L152-L158 https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L130-L136 https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L97 https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L87
AutoPxGmx.setPlatform and AutoPxGlp.setPlatform do not provide approve for new platform address. As a result both contracts will not be able to work normally as platform will not be able to transfer any funds from both contracts.
AutoPxGmx.setPlatform and AutoPxGlp.setPlatform allow for the owner to provide new platform address. However in this methods approve is not provided for new platform as it is done in constructors of both contracts. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L97
gmx.safeApprove(_platform, type(uint256).max);
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L87
gmxBaseReward.safeApprove(address(_platform), type(uint256).max);
As result after the owner will change platform contracts will not work normally as platform will not be able to transfer tokens from contracts. Also there is no any other method that can grant allowance, that means that owner will not be able to do anything, except return to new platform address.
VsCode
Grant allowance for the new platform in both methods.
#0 - c4-judge
2022-12-04T13:51:01Z
Picodes marked the issue as duplicate of #182
#1 - c4-judge
2023-01-01T11:06:43Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-01-01T11:29:09Z
Picodes changed the severity to 3 (High Risk)
#3 - c4-judge
2023-01-01T11:32:56Z
Picodes changed the severity to 2 (Med Risk)
🌟 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
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L104-L110
AutoPxGmx.setPoolFee doesn't check that pool with such fee exists, as result compound function will not be working and deposits and withdraws as well.
Function AutoPxGmx.setPoolFee allows to set pool fee for uniswap. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L104-L110
function setPoolFee(uint24 _poolFee) external onlyOwner { if (_poolFee == 0) revert ZeroAmount(); poolFee = _poolFee; emit PoolFeeUpdated(_poolFee); }
This poolFee is then used to make uniswap calls in compound function. However if uniswap can't finds pool with such fee then transaction will revert. As result user will not be able to make deposits and withdraws as they depend on compound function.
VsCode
Check that such pool exists in uniswap when setting new poolFee.
#0 - Picodes
2022-12-04T13:27:24Z
Safety check so falls within QA
#1 - c4-judge
2022-12-04T13:27:31Z
Picodes changed the severity to QA (Quality Assurance)
#2 - c4-judge
2022-12-05T09:54:27Z
Picodes marked the issue as grade-b