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: 15/101
Findings: 2
Award: $1,198.73
๐ Selected for report: 1
๐ Solo Findings: 0
๐ Selected for report: KingNFT
Also found by: 0x52, HE1M, ladboy233, rvierdiiev, xiaoming90
1173.4088 USDC - $1,173.41
https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexGmx.sol#L615 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexGmx.sol#L685 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexGmx.sol#L712
The following 'redeem' related functions are likely to be blocked, users will not be able to retrieve their funds.
function _redeemPxGlp( address token, uint256 amount, uint256 minOut, address receiver ); function redeemPxGlpETH( uint256 amount, uint256 minOut, address receiver ); function redeemPxGlp( address token, uint256 amount, uint256 minOut, address receiver );
The 'GlpManager' contract of GMX has a 'cooldownDuration' limit on redeem/unstake ('_removeLiquidity()'). While there is at least one deposit/stake ('_addLiquidity()') operation in the past 'cooldownDuration' time, redemption would fail. Obviously this limitation is user-based, and 'PirexGmx' contract is one such user.
Current setting of 'cooldownDuration' is 15 minutes, the max value is 2 days.
https://arbiscan.io/address/0x321f653eed006ad1c29d174e17d96351bde22649#readContract
Due to the above limit, there are 3 risks that can block redemption for Pirex users.
1.The normal case Let's say there is 10% GMX users will use Pirex to manage their GLP. By checking recent history of GMX router contract, we can find the average stake interval is smaller than 1 minute https://arbiscan.io/address/0xa906f338cb21815cbc4bc87ace9e68c87ef8d8f1 Let's take
averageStakeIntervalOfGMX = 30 seconds
So if Pirex has 10% of GMX users, then
averageStakeIntervalOfPirex = 30 รท 10% = 300 seconds
The probability of successfully redeeming is a typical Poisson distribution: https://en.wikipedia.org/wiki/Poisson_distribution With
ฮป = cooldownDuration รท averageStakeIntervalOfPirex = 15 * 60 รท 300 = 3 k = 0
So we get
P โ 1 รท (2.718 * 2.718 * 2.718) โ 0.05
Conclusion
If Pirex has 10 % of GMX users, then the redemption will fail with 95% probability.
A full list of % of GMX users versus failure probability of redemption
1% : 26% 5% : 78% 10% : 95% 20% : 99.75% 30% : 99.98%
2.The attack case If an attacker, such as bad competitors of similar projects, try to exploit this vulnerability. Let's estimate the cost for attack.
As attacker can deposit a very small GLP, such as 1 wei, so we can ignore the GLP cost and only focus on GAS cost.
By checking the explorer history https://arbiscan.io We are safe to assume the cost for calling 'depositGlpETH()' or 'depositGlp' is
txCost = 0.1 USD
To block redemption, attacker have to execute a deposit call every 15 minutes, so
dailyCost = 24 * (60 / 15) * 0.1 = 9.6 USD yearCost = 365 * 9.6 = 3504 USD
Conclusion
If an attacker want to block Pirex users funds, his yearly cost is only about 3.5k USD.
3.GMX adjusts protocol parameters If GMX increases 'cooldownDuration' to 2 days, it will obviously cause redemption not working.
VS Code
Reserve some time range for redemption only. e.g. 1 of every 7 days.
#0 - c4-judge
2022-12-03T20:30:29Z
Picodes marked the issue as duplicate of #110
#1 - c4-judge
2022-12-05T10:45:39Z
Picodes marked the issue as not a duplicate
#2 - c4-judge
2022-12-05T10:45:44Z
Picodes marked the issue as primary issue
#3 - c4-judge
2022-12-05T10:45:48Z
Picodes marked the issue as selected for report
#4 - c4-sponsor
2022-12-05T20:32:39Z
kphed marked the issue as sponsor confirmed
๐ 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
25.3241 USDC - $25.32
https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/PirexERC4626.sol#L60 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/PirexERC4626.sol#L80
There is a front running attack vector on users who call 'deposit()' or 'mint()' of 'PirexERC4626' based contracts ( 'AutoPxGlp' and 'AutoPxGmx') while the initial share of contract is 0. Attack can steal up to half of the user's deposit amount.
Let's take an example of 'AutoPxGmx' contract to explain the attack steps.
1.Victim user call 'deposit()' with parameter
assets = 2000 pxGMX = 2000e18 wei pxGMX
and the transaction is cached in the memory pool of node.
2.Attacker front running the following 2 transactions (1) send 1000 pxGMX to contract directly, now
totalAssets = 1000 pxGMX = 1000e18 wei pxGMX
(2) call 'deposit()' with parameter
assets = 1e-18 pxGMX = 1 wei pxGMX
Now
apxGMXBalanceOfAttacker = 1 wei apxGMX totalAssets = 1000e18 + 1 wei pxGMX totalSupply = 1 wei apxGMX
3.Victim's transaction executes, then
apxGMXBalanceOfVictim = assets * totalSupply / totalAssets = 2000e18 * 1 / (1000e18 + 1) = 1 wei apxGMX totalAssets = 3000e18 + 1 wei pxGMX totalSupply = 2 wei apxGMX
4.Attacker call 'redeem()' with
shares = 1 wei apxGMX
then
assets = shares * totalAssets / totalSupply = 1 * (3000e18 + 1) / 2 = 1500e18 = 1500 pxGMX penalty = assets * 300 / 10000 = 45 pxGMX
received
assets - penalty = 1500 - 45 = 1455 pxGMX
and profit
1455 - 1000 = 455 pxGMX
Attacker can draw out more token from victim by increasing deposit and shares, the up limit is half of the user's deposit amount.
Related functions:
function convertToAssets(uint256 shares) public view virtual returns (uint256) { uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply); } 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()); }
VS Code
Admin deposits some token in 'constructor' and never redeem.
constructor( address _gmxBaseReward, address _gmx, address _asset, string memory _name, string memory _symbol, address _platform, address _rewardsModule ) Owned(msg.sender) PirexERC4626(ERC20(_asset), _name, _symbol) { // ... deposit(0.01 ether, address(this)); // @fix }
#0 - c4-judge
2022-12-04T00:21:56Z
Picodes marked the issue as duplicate of #407
#1 - c4-judge
2022-12-21T07:20:13Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-01-01T11:36:24Z
Picodes changed the severity to 3 (High Risk)
#3 - C4-Staff
2023-01-10T21:54:30Z
JeeberC4 marked the issue as duplicate of #275