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: 17/101
Findings: 3
Award: $824.67
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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#L190 https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L212
The function previewWithdraw
is overridden in the AutoPxGmx
and AutoPxGlp
contracts to account for penalty fees while exiting the vaults. This happens in line 212 of the AutoPxGmx
contract and similarly in line 190 of the AutoPxGlp
contract:
return (_totalSupply == 0 || _totalSupply - shares == 0) ? shares : (shares * FEE_DENOMINATOR) / (FEE_DENOMINATOR - withdrawalPenalty);
Here, the integer division will round down, while the correct behavior should be to round up in favor of the protocol.
This issue would allow a user to withdraw tokens without paying penalties, which should not represent a big impact in a realistic scenario since it would need to involve handling extremely low quantities of assets/shares or withdrawing assets in batches of a relatively small amount (which should be discouraged by gas costs).
During each call to withdraw
, if the division is not exact, the protocol will be losing 1 share of penalties due to rounding.
The correct way should be to round up the division, following the base ERC4626 implementation of the previewWithdraw
function which uses mulDivUp
.
The following test demonstrates the issue. In this case the user mints 10 shares and then withdraws 1 token by burning a single share without any penalty.
// SPDX-License-Identifier: MIT pragma solidity 0.8.17; import "forge-std/Test.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {AutoPxGmx} from "src/vaults/AutoPxGmx.sol"; import {PirexGmx} from "src/PirexGmx.sol"; import {Helper} from "./Helper.sol"; contract AuditTest is Helper { function test_AutoPxGmx_WithdrawWrongRounding() public { address user = testAccounts[0]; uint256 userBalance = 10; _mintPx(user, userBalance, true); // Approve contract vm.prank(user); pxGmx.approve(address(autoPxGmx), type(uint256).max); // User deposits balance, he gets 10 shares vm.prank(user); autoPxGmx.deposit(userBalance, user); assertEq(autoPxGmx.balanceOf(user), userBalance); vm.prank(user); uint256 shares = autoPxGmx.previewWithdraw(1); // Only 1 share needed to withdraw 1 token assertEq(shares, 1); vm.prank(user); autoPxGmx.withdraw(1, user, user); // User has withdrawn 1 token and still has 9 shares assertEq(pxGmx.balanceOf(user), 1); assertEq(autoPxGmx.balanceOf(user), 9); } }
Change the penalty calculation in the previewWithdraw
function to round up:
return (_totalSupply == 0 || _totalSupply - shares == 0) ? shares : shares.mulDivUp(FEE_DENOMINATOR, FEE_DENOMINATOR - withdrawalPenalty);
#0 - c4-judge
2022-12-03T21:49:39Z
Picodes marked the issue as duplicate of #264
#1 - c4-judge
2023-01-01T10:45:17Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-01-01T11:34:26Z
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:42:53Z
Duplicate of #178
🌟 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
PirexRewards
Even though the contract doesn't expose any delegatecall
or way to call selfdestruct
(it uses transparent proxies instead of UUPS), consider adding a constructor to disable initializers in the implementation contract of PirexRewards
.
constructor() { _disableInitializers(); }
super
instead of duplicating code in transfer
and transferFrom
functions of PxERC20
contractBoth functions have the same code as their respective base ERC20 implementation with an added code to accrue rewards. Call the base implementation instead of duplicating all the code. For example:
function transfer(address to, uint256 amount) public override returns (bool) { bool result = super.transfer(to, amount); // Accrue rewards for sender, up to their current balance and kick off accrual for receiver pirexRewards.userAccrue(this, msg.sender); pirexRewards.userAccrue(this, to); return result; }
AutoPxGlp
constructorThe comment in line 86 reads:
// Approve the Uniswap V3 router to manage our base reward (inbound swap token)
However this seems to be a badly copy pasted comment from the AutoPxGmx contract. The approval is to the PirexGmx contract in order to compound the rewards.
super
instead of duplicating code in withdraw
and redeem
functions of AutoPxGmx
contractBoth functions have the same code as their respective base ERC4626 implementation with a prepended code to compound the vault. Call the base implementation instead of duplicating all the code. For example:
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); shares = super.withdraw(assets, receiver, owner); }
#0 - c4-judge
2022-12-04T20:37:55Z
Picodes marked the issue as grade-b
🌟 Selected for report: gzeon
Also found by: 0xPanda, 0xSmartContract, B2, Deivitto, Diana, JohnSmith, PaludoX0, Rahoz, RaymondFam, ReyAdmirado, Rolezn, Schlagatron, Secureverse, Tomio, __141345__, adriro, ajtra, aphak5010, c3phas, chaduke, codeislight, cryptonue, datapunk, dharma09, halden, karanctf, keccak123, oyc_109, pavankv, sakshamguruji, saneryee, unforgiven
604.661 USDC - $604.66
PirexGmx.depositGlp
functionThe token
parameter is checked to guard against the zero address, but then the same parameter is validated using a list of whitelisted addresses, which should also cover the zero address check.
stakedGlp
token in AutoPxGlp contractThe implementation of depositFsGlp
fetches the stakedGlp
token in every call using the PirexGmx contract (platform
). Since this is a fixed token from the GMX protocol, consider using an immutable variable defined at construction time.
stakedGlp
token in the AutoPxGlp contractThe function depositFsGlp
will approve the spending for the given amount in every call. Consider adding an infinite approval during contract construction, similar to how it's done with the gmxBaseReward
token.
totalAssets()
in compound
function of AutoPxGmx contractThe function fetches the total amount of assets in line 288 (in the if condition) and does the same calculation in line 290 (asset.balanceOf(address(this)) - assetsBeforeClaim
).
pxGmx
token can be an immutable variable in the PxGmxReward
contractThe pxGmx
variable is defined at construction and can't be modified by the contract. Consider using an immutable variable to save gas.
#0 - c4-judge
2022-12-05T11:01:14Z
Picodes marked the issue as grade-b
#1 - c4-judge
2022-12-05T11:01:41Z
Picodes marked the issue as grade-a
#2 - c4-sponsor
2022-12-09T05:15:26Z
drahrealm marked the issue as sponsor confirmed