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: 25/101
Findings: 4
Award: $448.44
๐ 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
41.6303 USDC - $41.63
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGlp.sol#L177-L195
Per EIP 4626's Security Considerations (https://eips.ethereum.org/EIPS/eip-4626)
Finally, ERC-4626 Vault implementers should be aware of the need for specific, opposing rounding directions across the different mutable and view methods, as it is considered most secure to favor the Vault itself during calculations over its users:
If (1) itโs calculating how many shares to issue to a user for a certain amount of the underlying tokens they provide or (2) itโs determining the amount of the underlying tokens to transfer to them for returning a certain amount of shares, it should round down. If (1) itโs calculating the amount of shares a user has to supply to receive a given amount of the underlying tokens or (2) itโs calculating the amount of underlying tokens a user has to provide to receive a certain amount of shares, it should round up.
The original implementation of previewWithdraw in solmate ERC4626 is:
function previewWithdraw(uint256 assets) public view virtual returns (uint256) { uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? assets : assets.mulDivUp(supply, totalAssets()); }
It is rounding up,but the implementation of autopxglp.sol#previewWith
rounds down
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); } 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()); // @audit }
it might cause some intergration problem in the future that can lead to wide range of issues for both parties.
vscode
Round up in previewWithdraw using mulDivUp and divWadUp
#0 - c4-judge
2022-12-04T00:24:58Z
Picodes marked the issue as duplicate of #264
#1 - c4-judge
2022-12-04T00:25:02Z
Picodes marked the issue as partial-25
#2 - Picodes
2022-12-04T00:25:13Z
No mention of how this could impact user funds
#3 - c4-judge
2023-01-01T11:34:37Z
Picodes changed the severity to 3 (High Risk)
#4 - C4-Staff
2023-01-10T21:57:13Z
JeeberC4 marked the issue as 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
25.3241 USDC - $25.32
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L370-L404
Most of the share based vault implementation will face this issue. The vault is based on the ERC4626 where the shares are calculated based on the deposit value. By depositing large amount as initial deposit, initial depositor can influence the future depositors value.
Shares are minted based on the deposit value. https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L370-L404 Public vault is based on the ERC4626 where the shares are calculated based on the deposit value.
Reference: https://github.com/sherlock-audit/2022-08-sentiment-judging#issue-h-1-a-malicious-early-userattacker-can-manipulate-the-ltokens-pricepershare-to-take- an-unfair-share-of-future-users-deposits:~:text=Issue%20H%2D1%3A%20A%20malicious%20early%20user/attacker%20can%20manipulate%20the%20LToken%27s%20pricePerShare%20to% 20take%20an%20unfair%20share%20of%20future%20users%27%20deposits https://github.com/sherlock-audit/2022-10-astaria-judging#issue-m-26-first-erc4626-deposit-can-break-share-calculation
function depositGmx(uint256 amount, address receiver) external nonReentrant returns (uint256 shares) { if (amount == 0) revert ZeroAmount(); if (receiver == address(0)) revert ZeroAddress(); // Handle compounding of rewards before deposit (arguments are not used by `beforeDeposit` hook) if (totalAssets() != 0) beforeDeposit(address(0), 0, 0); // Intake sender GMX gmx.safeTransferFrom(msg.sender, address(this), amount); // Convert sender GMX into pxGMX and get the post-fee amount (i.e. assets) (, uint256 assets, ) = PirexGmx(platform).depositGmx( amount, address(this) ); // NOTE: Modified `convertToShares` logic to consider assets already being in the vault // and handle it by deducting the recently-deposited assets from the total 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); } 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()); } function totalAssets() public view override returns (uint) { return asset.balanceOf(address(this)) + getBorrows() - getReserves(); }
Future depositors are forced for huge value of asset to deposit. It is not practically possible for all the users. This could directly affect on the attrition of users towards this system.
vscode
Consider requiring a minimal amount of share tokens to be minted for the first minter
#0 - c4-judge
2022-12-04T00:25:58Z
Picodes marked the issue as duplicate of #407
#1 - Picodes
2022-12-04T00:26:00Z
No PoC aside from the references
#2 - c4-judge
2023-01-01T11:07:26Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-01-01T11:35:08Z
Picodes changed the severity to 3 (High Risk)
#4 - C4-Staff
2023-01-10T21:54:30Z
JeeberC4 marked the issue as duplicate of #275
๐ 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#L97 https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/vaults/AutoPxGmx.sol#L152-L158
When the contract is created, the approve operation will be performed on gmx
gmxBaseReward.safeApprove(address(SWAP_ROUTER), type(uint256).max); gmx.safeApprove(_platform, type(uint256).max);
But the platform
address can be updated
function setPlatform(address _platform) external onlyOwner { if (_platform == address(0)) revert ZeroAddress(); platform = _platform; emit PlatformUpdated(_platform); }
Two issues are overlooked here
platform
can still operate the gmx
of the contract without revoking the authorization operation of the previous platform.approve()
operation for the new platform. It will cause compound()
and depositGmx()
to fail because there is no approve operation for the new platform
addresssrc/vaults/AutoPxGmx.sol: 280 // Deposit entire GMX balance for pxGMX, increasing the asset/share amount 281: (, pxGmxMintAmount, ) = PirexGmx(platform).depositGmx( 282 gmx.balanceOf(address(this)), 384 // Convert sender GMX into pxGMX and get the post-fee amount (i.e. assets) 385: (, uint256 assets, ) = PirexGmx(platform).depositGmx( 386 amount,
function setPlatform(address _platform) external onlyOwner { if (_platform == address(0)) revert ZeroAddress(); platform = _platform; emit PlatformUpdated(_platform); }
vscode
function setPlatform(address _platform) external onlyOwner { if (_platform == address(0)) revert ZeroAddress(); + gmx.safeApprove(platform, 0); platform = _platform; + gmx.safeApprove(platform, type(uint256).max); emit PlatformUpdated(_platform); }
#0 - c4-judge
2022-12-03T20:46:18Z
Picodes marked the issue as duplicate of #182
#1 - c4-judge
2023-01-01T11:09:56Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-01-01T11:32:35Z
Picodes changed the severity to 2 (Med Risk)
๐ Selected for report: unforgiven
Also found by: 8olidity
309.5412 USDC - $309.54
https://github.com/code-423n4/2022-11-redactedcartel/blob/main/src/PirexGmx.sol#L257
Consider the case where r.totalsupply is 0. When r.totalsupply is 0, it should return 0 directly, because there will be an error of dividing by 0.
function _calculateRewards(bool isBaseReward, bool useGmx) internal view returns (uint256) { RewardTracker r; uint256 cumulativeRewardPerToken = r.cumulativeRewardPerToken() + ((blockReward * precision) / r.totalSupply()); // @audit
vscode
if ( r.totalSupply() == 0) return 0;
#0 - c4-judge
2022-12-04T13:29:28Z
Picodes marked the issue as duplicate of #237
#1 - Picodes
2022-12-26T14:02:35Z
Partial credit as it does not explain what the impact is and it could affect the protocol's users
#2 - c4-judge
2022-12-26T14:02:44Z
Picodes marked the issue as partial-25
#3 - c4-judge
2022-12-26T14:02:57Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2022-12-26T14:03:14Z
Picodes marked the issue as partial-25