Redacted Cartel contest - 0xbepresent's results

Boosted GMX assets from your favorite liquid token wrapper, Pirex - brought to you by Redacted Cartel.

General Information

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

Redacted Cartel

Findings Distribution

Researcher Performance

Rank: 30/101

Findings: 3

Award: $193.05

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

7.9647 USDC - $7.96

Labels

bug
2 (Med Risk)
partial-50
duplicate-137

External Links

Lines of code

https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L242 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L227 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L321 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L345

Vulnerability details

Impact

The compound() function helps to swap the gmxBaseReward (ether) for GMX tokens then the GMX tokens are deposited for pxGMX. In the swap function the amountOutMinimum helps to put the minimum value that is expected for the swap. The fixed amountOutMInimum assumes that the price can not be manipulated by different techniques so it is better to calculate the minimum accepted amount.

The amountOutMinimum should be calculated with the Uniswap SDK or an onchain price oracle as their documentation says:

For a real deployment, this value should be calculated using our SDK or an onchain 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.

Proof of Concept

The following lines use a fixed amountOutMinimum value:

Tools used

VisualStudio

Calculates the amountOutMinimum using the Uniswap SDK, an onchain price oracle or a time weighted average price.

#0 - c4-judge

2022-12-03T22:07:56Z

Picodes marked the issue as duplicate of #185

#1 - c4-judge

2022-12-03T22:08:05Z

Picodes marked the issue as partial-50

#2 - Picodes

2022-12-03T22:08:33Z

The finding is valid but does not clearly explains the actual consequences of the current design choice

#3 - C4-Staff

2023-01-10T22:10:37Z

JeeberC4 marked the issue as duplicate of #137

Findings Information

🌟 Selected for report: Jeiwan

Also found by: 0xbepresent, Koolex, __141345__, cryptoDave, cryptonue, datapunk, pashov, unforgiven

Labels

bug
2 (Med Risk)
satisfactory
duplicate-271

Awards

131.6023 USDC - $131.60

External Links

Lines of code

https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexRewards.sol#L386-L387

Vulnerability details

Impact

The user can lost his rewards if the reward token is removed from the producerTokens[producerToken].rewardTokens list. If the reward token is removed, the rewardToken length is going to be zero, the user rewards going to be zero and the for statement will never enter so the user will lost his rewards for this token and the global state rewards will save inaccurate information.

Proof of Concept

  1. Add user pxGMX token an accrue his rewards
  2. Remove rewardToken
  3. The user won't get his rewards even if the reward token is added again

I did a test for this situation:

// test/PirexRewards.t.sol
function testClaimRewardsWithRemovedToken(
    uint32 secondsElapsed,
    uint8 multiplier,
    bool useETH
) external {
    vm.assume(secondsElapsed > 10);
    vm.assume(secondsElapsed < 365 days);
    vm.assume(multiplier != 0);
    vm.assume(multiplier < 10);
    address userX = testAccounts[0];
    uint256 gmxAmount = 100e18;

    _mintApproveGmx(gmxAmount, address(this), address(pirexGmx), gmxAmount);
    pirexGmx.depositGmx(gmxAmount, userX);

    assertEq(weth.balanceOf(userX), 0);

    vm.warp(block.timestamp + secondsElapsed);

    //
    // Add reward token and harvest rewards from Pirex contract
    //
    pirexRewards.addRewardToken(pxGmx, weth);
    pirexRewards.harvest();

    pirexRewards.userAccrue(pxGmx, userX);
    (, , uint256 globalRewardsBeforeClaimPxGmx) = _getGlobalState(
        pxGmx
    );
    (, , uint256 userRewardsBeforeClaimPxGmx) = pirexRewards
        .getUserState(pxGmx, userX);
    //
    // Sum of reward amounts that the user is entitled to
    //
    uint256 expectedClaimAmount = ((pirexRewards.getRewardState(
        pxGmx,
        weth
    ) * _calculateUserRewards(pxGmx, userX)) /
        _calculateGlobalRewards(pxGmx));

    //
    // Remove WETH from pxGMX
    //
    vm.expectEmit(true, false, false, true, address(pirexRewards));
    emit RemoveRewardToken(pxGmx, 0);
    pirexRewards.removeRewardToken(pxGmx, 0);

    // vm.expectEmit(true, false, false, false, address(pirexGmx));
    // emit ClaimUserReward(userX, address(0), 0, 0, 0);  // this emit is not called
    vm.expectEmit(true, true, false, true, address(pirexRewards));
    emit Claim(pxGmx, userX);
    pirexRewards.claim(pxGmx, userX);

    (, , uint256 globalRewardsAfterClaimPxGmx) = _getGlobalState(pxGmx);
    (, , uint256 userRewardsAfterClaimPxGmx) = pirexRewards.getUserState(pxGmx, userX);

    assertEq(
        globalRewardsBeforeClaimPxGmx - userRewardsBeforeClaimPxGmx,
        globalRewardsAfterClaimPxGmx
    );
    assertEq(0, userRewardsAfterClaimPxGmx);

    //
    // Owner weth balance has not changed. It is still zero.
    // userX has not received their rewards
    //
    assertEq(weth.balanceOf(userX), 0);
    assertTrue(expectedClaimAmount != weth.balanceOf(userX));

    // Add again the rewardToken
    pirexRewards.addRewardToken(pxGmx, weth);
    pirexRewards.harvest();
    
    // userX balance rewards is still zero
    pirexRewards.claim(pxGmx, userX);
    assertEq(weth.balanceOf(userX), 0);
}

Tools used

Foundry/VSCode

Add a check if the reward token exists or not so the p.userStates[user].rewards is not zero if the reward token was removed:

...
...
ERC20[] memory rewardTokens = p.rewardTokens;
uint256 rLen = rewardTokens.length;

// Claim should be skipped and not reverted on zero global/user reward
if (globalRewards != 0 && userRewards != 0 && rLen != 0) {
    // Update global and user reward states to reflect the claim
    p.globalState.rewards = globalRewards - userRewards;
    p.userStates[user].rewards = 0;
    ...
    ...
}

#0 - c4-judge

2022-12-03T22:11:17Z

Picodes marked the issue as duplicate of #255

#1 - c4-judge

2022-12-05T10:38:57Z

Picodes marked the issue as duplicate of #255

#2 - c4-judge

2023-01-01T10:39:59Z

Picodes marked the issue as duplicate of #271

#3 - c4-judge

2023-01-01T10:45:00Z

Picodes marked the issue as satisfactory

1 - Use totalAssets() function.

Use the totalAssets() function in the corresponding contract.

Instances of this issue:

2 - Save the newAssets calculation in a variable and use it in the condition and next calculation

Save the newAssets calculation in a variable here src/vaults/AutoPxGmx.sol#L288 and use it in the 290 line. Basically it follows the same pattern from src/vaults/AutoPxGlp.sol#L250

Example:

uint256 newAssets = totalAssets() - assetsBeforeClaim;
if (newAssets != 0) {
	totalFee = (newAssets * platformFee) / FEE_DENOMINATOR;
}

#0 - c4-judge

2022-12-05T08:54:06Z

Picodes marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter