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: 16/101
Findings: 4
Award: $1,058.24
๐ Selected for report: 1
๐ 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#L184
EIP4626 Rounding Specification (https://eips.ethereum.org/EIPS/eip-4626)
Thus, the result of theย previewMint
ย andย previewWithdraw
ย should be rounded up
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 current implementation of convertToShares
function will round down the number of shares returned due to how solidity handles Integer Division. ERC4626 expects the returned value of convertToShares
to be rounded down. Thus, this function behaves as expected:
src/vaults/PirexERC4626.sol: 155 156: function convertToShares(uint256 assets) 157: public 158: view 159: virtual 160: returns (uint256) 161: { 162: uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. 163: 164: return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets()); 165: }
ERC 4626 expects the result returned from previewWithdraw
function to be rounded up. However, within the previewWithdraw
function, it calls the convertToShares
function
Recall earlier that the convertToShares
function returned a rounded down value, thus previewWithdraw
will return a rounded down value instead of round up value.
Thus, this function does not behave as expected
src/vaults/AutoPxGmx.sol: 200 */ 201: function previewWithdraw(uint256 assets) 202: public 203: view 204: override 205: returns (uint256) 206: { 207: // Calculate shares based on the specified assets' proportion of the pool 208: uint256 shares = convertToShares(assets); 209: 210: // Save 1 SLOAD 211: uint256 _totalSupply = totalSupply; 212: 213: // Factor in additional shares to fulfill withdrawal if user is not the last to withdraw 214: return 215: (_totalSupply == 0 || _totalSupply - shares == 0) 216: ? shares 217: : (shares * FEE_DENOMINATOR) / 218: (FEE_DENOMINATOR - withdrawalPenalty); 219: }
Manual Code Review
Ensure that the rounding of vault's functions behave as expected. Following are the expected rounding direction for each vault function:
previewMint(uint256 shares) - Round Up โฌ
previewWithdraw(uint256 assets) - Round Up โฌ
previewRedeem(uint256 shares) - Round Down โฌ
previewDeposit(uint256 assets) - Round Down โฌ
convertToAssets(uint256 shares) - Round Down โฌ
convertToShares(uint256 assets) - Round Down โฌ
previewMint
returns the amount of assets that would be deposited to mint specific amount of shares. Thus, the amount of assets must be rounded up, so that the vault won't be shortchanged.
previewWithdraw
returns the amount of shares that would be burned to withdraw specific amount of asset. Thus, the amount of shares must to be rounded up, so that the vault won't be shortchanged.
Alternatively, if such alignment of rounding could not be achieved due to technical limitation, at the minimum, document this limitation in the comment so that the developer performing the integration is aware of this.
#0 - c4-judge
2022-12-04T00:07:10Z
Picodes marked the issue as duplicate of #264
#1 - c4-judge
2022-12-04T00:07:15Z
Picodes marked the issue as partial-25
#2 - Picodes
2022-12-04T00:07:34Z
Does not demonstrate how it could lead to a break in functionality or loss of fund
#3 - c4-judge
2023-01-01T11:34:33Z
Picodes changed the severity to 3 (High Risk)
#4 - C4-Staff
2023-01-10T21:57:30Z
JeeberC4 marked the issue as duplicate of #264
#5 - liveactionllama
2023-01-11T18:43:31Z
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-L403
A well known attack vector for almost all shares based liquidity pool contracts, where an early user can manipulate the price per share and profit from late users' deposits because of the precision loss caused by the rather large value of price per share.
depositGMX()
function has 0 shares protect. But it hasn't few shares protect. For example; It's like getting 1 share after deposit with high token;
if ( (shares = supply == 0 ? assets : assets.mulDivDown(supply, totalAssets() - assets)) == 0 ) revert ZeroShares();
1 - A malicious early user canย depositGMX()
ย withย 1 weiย ofย assetย token as the first depositor of the LToken, and getย 1 weiย of shares
2 - Then the attacker can sendย 10000e18 - 1ย ofย assetย tokens and inflate the price per share from 1.0000 to an extreme value of 1.0000e22 ( fromย (1 + 10000e18 - 1) / 1)
3 - As a result, the future user who depositsย 19999e18ย will only receiveย 1 weiย (fromย 19999e18 * 1 / 10000e18) of shares token
4 - They will immediately loseย 9999e18ย or half of their deposits if theyย redeem()
ย right after theย depositGMX()
The attacker can profit from future users' deposits. While the late users will lose part of their funds to the attacker.
Consider requiring a minimal amount of share tokens to be minted for the first minter, and send a port of the initial mints as a reserve to the Redacted MultiSig so that the pricePerShare can be more resistant to manipulation
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(); + /** for the first mint, we require the mint amount > (10 ** decimals) / 100 and send (10 ** decimals) / + 1_000_000 of the initial supply as a reserve to Redacted MultiSig + */ + if (totalSupply == 0 && decimals >= 6) { + require(shares > 10 ** (decimals - 2)); + uint256 reserveShares = 10 ** (decimals - 6); + _mint(Redacted_MultiSig, reserveShares); + shares -= reserveShares; + } _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); }
#0 - c4-judge
2022-12-04T12:35:45Z
Picodes marked the issue as duplicate of #407
#1 - c4-judge
2022-12-21T07:20:32Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-01-01T11:36:28Z
Picodes changed the severity to 3 (High Risk)
#3 - C4-Staff
2023-01-10T21:54:30Z
JeeberC4 marked the issue as duplicate of #275
๐ 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
951.6419 USDC - $951.64
Number | Issues Details | Context |
---|---|---|
[L-01] | PirexERC4626's implmentation is not fully up to EIP-4626's specification | 1 |
[L-02] | initialize() function can be called by anybodyย | 1 |
[L-03] | Solmate's SafeTransferLib doesn't check whether the ERC20 contract exists | 19 |
[L-04] | Use Ownable2StepUpgradeable instead of OwnableUpgradeable contract | 1 |
[L-05] | Owner can renounce Ownership | 1 |
[L-06] | Critical Address Changes Should Use Two-step Procedure | 4 |
[L-07] | DepositGlp Event arguments names are confusing | 2 |
[L-08] | Loss of precision due to rounding | 1 |
[L-09] | First value check of argument of type enum in setFee function is missing | 1 |
[L-10] | Hardcode the address causes no future updates | 1 |
[L-11] | Lack of Input Validation | 6 |
Total 11 issues
Number | Issues Details | Context |
---|---|---|
[N-01] | Insufficient coverage | 1 |
[N-02] | Not using the named return variables anywhere in the function is confusing | 1 |
[N-03] | Same Constant redefined elsewhere | 4 |
[N-04] | Omissions in Events | 8 |
[N-05] | Add parameter to Event-Emit | 1 |
[N-06] | NatSpec is missing | 1 |
[N-07] | Use require instead of assert | 1 |
[N-08] | Implement some type of version counter that will be incremented automatically for contract upgrades | 1 |
[N-09] | Constant values such as a call to keccak256(), should used to immutable rather than constant | 2 |
[N-10] | For functions, follow Solidity standard naming conventions | 4 |
[N-11] | Mark visibility ofย initialize(...)ย functions asย external | 1 |
[N-12] | No same value input control | 8 |
[N-13] | Include return parameters in NatSpec comments | All |
[N-14] | 0 address check for asset | 1 |
[N-15] | Use a single file for all system-wide constants | 6 |
[N-16] | Function writing that does not comply with the Solidity Style Guide | All |
[N-17] | Missing Upgrade Path for PirexRewards Implementation | 1 |
[N-18] | No need assert check in _computeAssetAmounts() | 1 |
[N-19] | Lack of event emission after criticalย initialize() ย functions | 1 |
[N-20] | Add a timelock to critical functions | 11 |
Total 19 issues
Number | Suggestion Details |
---|---|
[S-01] | Generate perfect code headers every time |
[S-02] | Add NatSpec comments to the variables defined in Storage |
Total 2 suggestions
Must return the maximum amount of shares mint would allow to be deposited to receiver and not cause a revert, which must not be higher than the actual maximum that would be accepted (it should underestimate if necessary).
This assumes that the user has infinite assets, i.e. must not rely on balanceOf
of asset.
src/vaults/PirexERC4626.sol: 216 217: function maxDeposit(address) public view virtual returns (uint256) { 218: return type(uint256).max; 219: } 220: 221: function maxMint(address) public view virtual returns (uint256) { 222: return type(uint256).max; 223: }
Could cause unexpected behavior in the future due to non-compliance with EIP-4626 standard.
Similar problem for Sentimentxyz ; https://github.com/sentimentxyz/protocol/pull/235/files
maxMint() and maxDeposit() should reflect the limitation of maxSupply.
Consider changing maxMint() and maxDeposit() to:
function maxMint(address) public view virtual returns (uint256) { if (totalSupply >= maxSupply) { return 0; } return maxSupply - totalSupply; } function maxDeposit(address) public view virtual returns (uint256) { return convertToAssets(maxMint(address(0))); }
initialize()
function can be called anybody when the contract is not initialized.
More importantly, if someone else runs this function, they will have full authority because of the __Ownable_init()
function.
Here is a definition of initialize()
function.
src/PirexRewards.sol: 84 85: function initialize() public initializer { 86: __Ownable_init(); 87: }
Add a control that makes initialize()
only call the Deployer Contract or EOA;
if (msg.sender != DEPLOYER_ADDRESS) { revert NotDeployer(); }
Solmate's SafeTransferLib, which is often used to interact with non-compliant/unsafe ERC20 tokens, does not check whether the ERC20 contract exists. The following code will not revert in case the token doesn't exist (yet).
This is stated in the Solmate library: https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol#L9
19 results src/PirexFees.sol: 5: import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; 114: token.safeTransfer(treasury, treasuryDistribution); 115: token.safeTransfer(contributors, contributorsDistribution); src/PirexGmx.sol: 7: import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; 392: gmx.safeTransferFrom(msg.sender, address(this), amount); 506: t.safeTransferFrom(msg.sender, address(this), tokenAmount); 643: ERC20(pxGlp).safeTransferFrom( 844: gmxBaseReward.safeTransfer(receiver, postFeeAmount); 847: gmxBaseReward.safeTransfer(address(pirexFees), feeAmount); 946: gmxBaseReward.safeTransfer( src/vaults/AutoPxGlp.sol: 6: import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; 258: asset.safeTransfer(msg.sender, pxGlpIncentive); 259 260: asset.safeTransfer(owner, totalPxGlpFee - pxGlpIncentive); 274: pxGmx.safeTransfer(msg.sender, pxGmxIncentive); 275 276: pxGmx.safeTransfer(owner, totalPxGmxFee - pxGmxIncentive); 344: stakedGlp.safeTransferFrom(msg.sender, address(this), amount); 387: erc20Token.safeTransferFrom(msg.sender, address(this), tokenAmount); 388 src/vaults/AutoPxGmx.sol: 7: import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; 297: if (incentive != 0) asset.safeTransfer(msg.sender, incentive); 298 299: asset.safeTransfer(owner, totalFee - incentive); 336: asset.safeTransfer(receiver, assets); 361: asset.safeTransfer(receiver, assets); 382: gmx.safeTransferFrom(msg.sender, address(this), amount);
Add a contract exist control in functions;
pragma solidity >=0.8.0; function isContract(address _addr) private returns (bool isContract) { isContract = _addr.code.length > 0; }
Ownable2StepUpgradeable
instead of OwnableUpgradeable
contractContext: PirexRewards.sol#L4
Description:
transferOwnership
function is used to change Ownership from OwnableUpgradeable.sol
.
There is another Openzeppelin Ownable contract (Ownable2StepUpgradeable.sol) has transferOwnership
function , use it is more secure due to 2-stage ownership transfer.
Context: PirexRewards.sol#L4
Description: Typically, the contractโs owner is the account that deploys the contract. As a result, the owner is able to perform certain privileged activities.
The Openzeppelinโsย Ownableย used inย this projectย contract implementsย renounceOwnership. This can represent a certain risk if the ownership is renounced for any other reason than by design. Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner.
onlyOwner
functions;
src/PirexRewards.sol: 93: function setProducer(address _producer) external onlyOwner { 151: function addRewardToken(ERC20 producerToken, ERC20 rewardToken) external onlyOwner
Recommendation: We recommend to either reimplement the function to disable it or to clearly specify if it is part of the contract design.
The critical procedures should be two step process.
src/PirexFees.sol: 63: function setFeeRecipient(FeeRecipient f, address recipient) src/PirexGmx.sol: 313: function setContract(Contracts c, address contractAddress) 884: function setVoteDelegate(address voteDelegate) external onlyOwner { src/external/DelegateRegistry.sol: 18: function setDelegate(bytes32 id, address delegate) public {
Recommended Mitigation Steps Lack of two-step procedure for critical operations leaves them error-prone. Consider adding two step procedure on the critical functions.
DepositGlp
Event arguments names are confusingThe uint256 amount
argument of the depositFsGlp
function in the PirexGmx.sol contract is named deposited
in the event DepositGlp
parameter. In emit DepositGlp
this causes confusion in terms of user, code readability and web pages.
src/PirexGmx.sol: 421 */ 422: function depositFsGlp(uint256 amount, address receiver) 423: external 424: whenNotPaused 425: nonReentrant 426: returns ( 427: uint256, 428: uint256, 429: uint256 430: ) 452: emit DepositGlp( 453: msg.sender, // caller 454: receiver, // receiver 455: address(stakedGlp), // token 456: 0, // tokenAmount 457: 0, // minUsdg 458: 0, // minGlp 459: amount, // deposited 460: postFeeAmount, // postFeeAmount 461: feeAmount // feeAmount 462: );
Recommended Mitigation Steps Event-Emit parameter names must match the function arguments or the argument they take value from.
src/PirexGmx.sol: 221 { 222: feeAmount = (assets * fees[f]) / FEE_DENOMINATOR; 223 postFeeAmount = assets - feeAmount;;
src/PirexGmx.sol: 298 @param fee uint256 Fee amount 299: */ 300: function setFee(Fees f, uint256 fee) external onlyOwner { 301: if (fee > FEE_MAX) revert InvalidFee(); 302: 303: fees[f] = fee; 304: 305: emit SetFee(f, fee); 306: }
Leaving the enum type argument Fees
in the setFee function empty and returning the value 0
gives the same result as returning the value 0
, this is a property of the enum type and therefore error-prone
Recommended Mitigation Step;
Use struck instead of enum
src/vaults/AutoPxGmx.sol: 18 IV3SwapRouter public constant SWAP_ROUTER = 19: IV3SwapRouter(0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45);
Router etc. In case the addresses change due to reasons such as updating their versions in the future, addresses coded as constants cannot be updated, so it is recommended to add the update option with the onlyOwner modifier.
For defence-in-depth purpose, it is recommended to perform additional validation against the amount that the user is attempting to deposit, mint, withdraw and redeem to ensure that the submitted amount is valid.
OpenZeppelinTokenizedVault.sol#L9
src/PirexGmx.sol: 429 */ 430: function depositGmx(uint256 amount, address receiver) 431: external 432: whenNotPaused 433: nonReentrant 434: returns ( 435: uint256, 436: uint256, 437: uint256 438: ) 439: { + require(amount <= maxDeposit(receiver), "deposit more than max");
src/vaults/PirexERC4626.sol: 79 80: function mint(uint256 shares, address receiver) 81: public 82: virtual 83: returns (uint256 assets) 84: { + require(shares <= maxMint(receiver), "mint more than max");
src/vaults/AutoPxGlp.sol: 438: function withdraw( 439: uint256 assets, 440: address receiver, 441: address owner 442: ) public override returns (uint256 shares) { + require(assets <= maxWithdraw(owner), "withdraw more than max"); src/vaults/AutoPxGmx.sol: 317: function withdraw( 318: uint256 assets, 319: address receiver, 320: address owner 321: ) public override returns (uint256 shares) { + require(assets <= maxWithdraw(owner), "withdraw more than max");
src/vaults/AutoPxGlp.sol: 450 */ 451: function redeem( 452: uint256 shares, 453: address receiver, 454: address owner 455: ) public override returns (uint256 assets) { + require(shares <= maxRedeem(owner), "redeem more than max"); src/vaults/AutoPxGmx.sol: 340 341: function redeem( 342: uint256 shares, 343: address receiver, 344: address owner 345: ) public override returns (uint256 assets) { + require(shares <= maxRedeem(owner), "redeem more than max");
Description: Testing all functions is best practice in terms of security criteria.
| File | % Lines | % Statements | % Branches | % Funcs | |-----------------------------------|-------------------|-------------------|------------------|-----------------| | src/PirexRewards.sol | 98.98% (97/98) | 99.24% (131/132) | 94.44% (51/54) | 94.12% (16/17) | | src/PxGmx.sol | 100.00% (0/0) | 100.00% (0/0) | 100.00% (0/0) | 0.00% (0/1) | | src/vaults/AutoPxGlp.sol | 91.11% (82/90) | 93.39% (113/121) | 87.04% (47/54) | 94.44% (17/18) | | src/vaults/AutoPxGmx.sol | 89.39% (59/66) | 91.46% (75/82) | 71.05% (27/38) | 92.31% (12/13) | | src/vaults/PirexERC4626.sol | 75.00% (39/52) | 76.79% (43/56) | 37.50% (6/16) | 47.62% (10/21) | | src/vaults/PxGmxReward.sol | 78.12% (25/32) | 79.49% (31/39) | 50.00% (5/10) | 75.00% (3/4) | | Total | 56.72% (523/922) | 58.93% (660/1120) | 59.95% (241/402) | 47.87% (90/188) |
Due to its capacity, test coverage is expected to be 100%
function getUserState(ERC20 producerToken, address user) external view returns ( uint256 lastUpdate, uint256 lastBalance, uint256 rewards ) { UserState memory userState = producerTokens[producerToken].userStates[ user ]; return (userState.lastUpdate, userState.lastBalance, userState.rewards); }
Recommendation: Consider adopting a consistent approach to return values throughout the codebase by removing all named return variables, explicitly declaring them as local variables, and adding the necessary return statements where appropriate. This would improve both the explicitness and readability of the code, and it may also help reduce regressions during future code refactors.
Keeping the same constants in different files may cause some problems or errors, reading constants from a single file is preferable. This should also be preferred in gas optimization
src/vaults/AutoPxGlp.sol: 17 18: uint256 public constant MAX_WITHDRAWAL_PENALTY = 500; 19: uint256 public constant MAX_PLATFORM_FEE = 2000; 20: uint256 public constant FEE_DENOMINATOR = 10000; 21: uint256 public constant MAX_COMPOUND_INCENTIVE = 5000; src/vaults/AutoPxGmx.sol: 20: uint256 public constant MAX_WITHDRAWAL_PENALTY = 500; 21: uint256 public constant MAX_PLATFORM_FEE = 2000; 22: uint256 public constant FEE_DENOMINATOR = 10000; 23: uint256 public constant MAX_COMPOUND_INCENTIVE = 5000;
Throughout the codebase, events are generally emitted when sensitive changes are made to the contracts. However, some events are missing important parameters
The events should include the new value and old value where possible:
Events with no old value;
8 results src/PirexFees.sol: 63: function setFeeRecipient(FeeRecipient f, address recipient) 83: function setTreasuryFeePercent(uint8 _treasuryFeePercent) src/PirexGmx.sol: 300: function setFee(Fees f, uint256 fee) external onlyOwner { 301 if (fee > FEE_MAX) revert InvalidFee(); 313: function setContract(Contracts c, address contractAddress) 862: function setDelegationSpace( 863 string memory _delegationSpace, 884: function setVoteDelegate(address voteDelegate) external onlyOwner { 885 if (voteDelegate == address(0)) revert ZeroAddress(); 909: function setPauseState(bool state) external onlyOwner { 910 if (state) { src/PirexRewards.sol: 93: function setProducer(address _producer) external onlyOwner { 94 if (_producer == address(0)) revert ZeroAddress();
Some event-emit description hasnโt parameter. Add to parameter for front-end website or client app , they can has that something has happened on the blockchain.
Events with no old value;
src/PirexGmx.sol: 894 */ 895: function clearVoteDelegate() public onlyOwner { 896: emit ClearVoteDelegate();
Description: NatSpec is missing for the following functions , constructor and modifier:
Context:
src/vaults/PxGmxReward.sol: 15: ERC20 public pxGmx; 17: GlobalState public globalState; 18: uint256 public rewardState; 19: mapping(address => UserState) public userRewardStates;
require
instead of assert
src/PirexGmx.sol: 224 225: assert(feeAmount + postFeeAmount == assets); 226 }
Description:
Assert should not be used except for tests, require
should be used
Prior to Solidity 0.8.0, pressing a confirm consumes the remainder of the process's available gas instead of returning it, as request()/revert() did.
assert() and ruqire();
The big difference between the two is that the assert()
function when false, uses up all the remaining gas and reverts all the changes made.
Meanwhile, a require()
function when false, also reverts back all the changes made to the contract but does refund all the remaining gas fees we offered to pay.This is the most common Solidity function used by developers for debugging and error handling.
Assertion() should be avoided even after solidity version 0.8.0, because its documentation states "The Assert function generates an error of type Panic(uint256). Code that works properly should never Panic, even on invalid external input. If this happens, you need to fix it in your contract. there's a mistake".
I suggest implementing some kind of version counter that will be incremented automatically when you upgrade the contract.
Recommendation:
uint256 public authorizeUpgradeCounter; function upgradeTo(address _newImplementation) external onlyOwner { _setPendingImplementation(_newImplementation); authorizeUpgradeCounter+=1; }
There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts.
While it doesn't save any gas because the compiler knows that developers often make this mistake, it's still best to use the right tool for the task at hand.
Constants should be used for literal values written into the code, and immutable variables should be used for expressions, or values calculated in, or passed into the constructor.
2 results src/PxERC20.sol: 9: bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 10: bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
Context: AutoPxGlp.sol#L487 AutoPxGlp.sol#L501 AutoPxGlp.sol#L474 AutoPxGlp.sol#L462
Description: The above codes don't follow Solidity's standard naming convention,
internal and private functions : the mixedCase format starting with an underscore (_mixedCase starting with an underscore)
external
Description: If someone wants to extend via inheritance, it might make more sense that the overridden initialize(...) function calls the internal {...}_init function, not the parent public initialize(...) function.
External instead of public would give more the sense of the initialize(...) functions to behave like a constructor (only called on deployment, so should only be called externally)
Security point of view, it might be safer so that it cannot be called internally by accident in the child contract
It might cost a bit less gas to use external over public
It is possible to override a function from external to public (= "opening it up") โ but it is not possible to override a function from public to external (= "narrow it down"). โ
For above reasons you can change initialize(...) to external
https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3750
src/PirexFees.sol: 63: function setFeeRecipient(FeeRecipient f, address recipient) 83: function setTreasuryFeePercent(uint8 _treasuryFeePercent) src/PirexGmx.sol: 313: function setContract(Contracts c, address contractAddress) 884: function setVoteDelegate(address voteDelegate) external onlyOwner { 909: function setPauseState(bool state) external onlyOwner { src/PirexRewards.sol: 93: function setProducer(address _producer) external onlyOwner { 107: function setRewardRecipient( 432: function setRewardRecipientPrivileged(
Recommendation:
Add code like this;
if (oracle == _oracle revert ADDRESS_SAME();
return parameters
in NatSpec commentsContext: All contracts
Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
If Return parameters are declared, you must prefix them with "/// @return".
Some code analysis programs do analysis by reading NatSpec details, if they can't see the "@return" tag, they do incomplete analysis.
Recommendation: Include return parameters in NatSpec comments
Recommendation Code Style:
/// @notice information about what a function does /// @param pageId The id of the page to get the URI for. /// @return Returns a page's URI if it has been minted function tokenURI(uint256 pageId) public view virtual override returns (string memory) { if (pageId == 0 || pageId > currentId) revert("NOT_MINTED"); return string.concat(BASE_URI, pageId.toString()); }
0 address
check for asset
Context:
src/vaults/PirexERC4626.sol: 47 48: constructor( 49: ERC20 _asset, 50: string memory _name, 51: string memory _symbol 52: ) ERC20(_name, _symbol, _asset.decimals()) { 53: asset = _asset; 54: }
Description: Also check of the address to protect the code from 0x0 address problem just in case. This is best practice or instead of suggesting that they verify address != 0x0, you could add some good NatSpec comments explaining what is valid and what is invalid and what are the implications of accidentally using an invalid address.
Recommendation:
like this;
if (_asset == address(0)) revert ADDRESS_ZERO();
There are many addresses and constants used in the system. It is recommended to put the most used ones in one file (for example constants.sol, use inheritance to access these values)
This will help with readability and easier maintenance for future changes. This also helps with any issues, as some of these hard-coded values are admin addresses.
constants.left Use and import this file in contracts that require access to these values. This is just a suggestion, in some use cases this may result in higher gas usage in the distribution.
21 results - 6 files src/PirexFees.sol: 20: uint8 public constant FEE_PERCENT_DENOMINATOR = 100; 23: uint8 public constant MAX_TREASURY_FEE_PERCENT = 75; src/PirexGmx.sol: 44: uint256 public constant FEE_DENOMINATOR = 1_000_000; 47: uint256 public constant FEE_MAX = 200_000; src/PxERC20.sol: 9: bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 10: bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); src/external/RewardTracker.sol: 772: uint256 public constant BASIS_POINTS_DIVISOR = 10000; 773: uint256 public constant PRECISION = 1e30; 775: uint8 public constant decimals = 18; src/vaults/AutoPxGlp.sol: 18: uint256 public constant MAX_WITHDRAWAL_PENALTY = 500; 19: uint256 public constant MAX_PLATFORM_FEE = 2000; 20: uint256 public constant FEE_DENOMINATOR = 10000; 21: uint256 public constant MAX_COMPOUND_INCENTIVE = 5000; 22: uint256 public constant EXPANDED_DECIMALS = 1e30; 23 src/vaults/AutoPxGmx.sol: 18: IV3SwapRouter public constant SWAP_ROUTER = 20: uint256 public constant MAX_WITHDRAWAL_PENALTY = 500; 21: uint256 public constant MAX_PLATFORM_FEE = 2000; 22: uint256 public constant FEE_DENOMINATOR = 10000; 23: uint256 public constant MAX_COMPOUND_INCENTIVE = 5000;
Function writing
that does not comply with the Solidity Style Guide
Context: All Contracts
Description: Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
Functions should be grouped according to their visibility and ordered:
constructor receive function (if exists) fallback function (if exists) external public internal private within a grouping, place theย viewย andย pureย functions last
PirexRewards
Implementationsrc/PirexRewards.sol: 4: import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; 85: function initialize() public initializer {
With the current and contracts, it is not possible to upgrade the contract.
Recommendation: Document the operational plan for contract upgrades. Also, consider using the UUPS proxy pattern to upgrade contract implementation.
assert
check in _computeAssetAmounts()
The assert
check in this function is not needed. Because this check will always be true because of this line:
postFeeAmount = assets - feeAmount;
Context:
src/PirexGmx.sol: 216 */ 217: function _computeAssetAmounts(Fees f, uint256 assets) 218: internal 219: view 220: returns (uint256 postFeeAmount, uint256 feeAmount) 221: { 222: 223: 224: feeAmount = (assets * fees[f]) / FEE_DENOMINATOR; 225: postFeeAmount = assets - feeAmount; 226: - 227: assert(feeAmount + postFeeAmount == assets); 228: }
initialize()
ย functionsTo record the init parameters for off-chain monitoring and transparency reasons, please consider emitting an event after theย initialize()
ย functions:
src/PirexRewards.sol: 84 85: function initialize() public initializer { 86: __Ownable_init(); 87: }
It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious owner making a sandwich attack on a user). Consider adding a timelock to:
11 results src/PirexGmx.sol: 351 */ 352: function setFee(Fees f, uint256 fee) external onlyOwner { 353 if (fee > FEE_MAX) revert InvalidFee(); 364 */ 365: function setContract(Contracts c, address contractAddress) 366 external 918 */ 919: function setDelegationSpace( 920 string memory _delegationSpace, 940 */ 941: function setVoteDelegate(address voteDelegate) external onlyOwner { 942 if (voteDelegate == address(0)) revert ZeroAddress(); 965 */ 966: function setPauseState(bool state) external onlyOwner { 967 if (state) { src/vaults/AutoPxGlp.sol: 94: function setWithdrawalPenalty(uint256 penalty) external onlyOwner { 95 if (penalty > MAX_WITHDRAWAL_PENALTY) revert ExceedsMax(); 106: function setPlatformFee(uint256 fee) external onlyOwner { 107 if (fee > MAX_PLATFORM_FEE) revert ExceedsMax(); src/vaults/AutoPxGmx.sol: 104: function setPoolFee(uint24 _poolFee) external onlyOwner { 105 if (_poolFee == 0) revert ZeroAmount(); 116: function setWithdrawalPenalty(uint256 penalty) external onlyOwner { 117 if (penalty > MAX_WITHDRAWAL_PENALTY) revert ExceedsMax(); 128: function setPlatformFee(uint256 fee) external onlyOwner { 129 if (fee > MAX_PLATFORM_FEE) revert ExceedsMax(); 152: function setPlatform(address _platform) external onlyOwner { 153 if (_platform == address(0)) revert ZeroAddress();
Description: I recommend using header for Solidity code layout and readability
https://github.com/transmissions11/headers
/*////////////////////////////////////////////////////////////// TESTING 123 //////////////////////////////////////////////////////////////*/
Description: I recommend adding NatSpec comments explaining the variables defined in Storage, their slots, their contents and definitions.
This improves code readability and control quality
Current Code;
src/vaults/AutoPxGlp.sol: 18: uint256 public constant MAX_WITHDRAWAL_PENALTY = 500; 19: uint256 public constant MAX_PLATFORM_FEE = 2000; 20: uint256 public constant FEE_DENOMINATOR = 10000; 21: uint256 public constant MAX_COMPOUND_INCENTIVE = 5000; 22: uint256 public constant EXPANDED_DECIMALS = 1e30; 24: uint256 public withdrawalPenalty = 300; 25: uint256 public platformFee = 1000; 26: uint256 public compoundIncentive = 1000; 27: address public platform; 30: address public immutable rewardsModule;
Recommendation Code;
//****** Slot 0 ******// 26: uint256 public constant MAX_WITHDRAWAL_PENALTY = 500; / /****** Slot 1 ******// 30: uint256 public constant MAX_PLATFORM_FEE = 2000; //****** Slot 2 ******// 33: uint256 public constant FEE_DENOMINATOR = 10000; ... /****** End of storage ******/
#0 - c4-judge
2022-12-05T09:44:30Z
Picodes marked the issue as grade-a
#1 - c4-judge
2023-01-01T10:31:10Z
Picodes marked the issue as selected for report
#2 - Picodes
2023-01-17T10:56:57Z
L-02 is NC at best considering for upgradeable contracts there is no way to setup an access control before calling initialize
L-07 is NC for me
L-10 is NC in the absence of impact
NC-05 is invalid
๐ 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
39.6537 USDC - $39.65
Number | Optimization Details | Context |
---|---|---|
[G-01] | Remove the initializer modifier [20 K Gas Saved] | 1 |
[G-02] | Packing Storage | 4 |
[G-03] | No need assert check in _computeAssetAmounts() [19 K Gas Saved ] | 1 |
[G-04] | Use assembly to write address storage values | 27 |
[G-05] | The solady Library's Ownable contract is significantly gas-optimized, which can be used | 2 |
[G-06] | Setting The Constructor To Payable | 8 |
[G-07] | Functions guaranteed to revert_ when callled by normal users can be marked payable ย | 8 |
[G-08] | Project file uses Transparent Proxy model but UUPS model is cheaper | 1 |
[G-09] | Optimize names to save gas | All |
[G-10] | Remove tokenAmount and msg.value check in the _depositGlp function [3 K Gas Saved ] | 1 |
Total 10 issues
Number | Suggestion Details |
---|---|
[S-01] | Use v4.8.0 OpenZeppelin contracts |
Total 1 suggestion
initializer
modifier [20 K Gas Saved]if we can just ensure that the initialize()
function could only be called from within the constructor, we shouldn't need to worry about it getting called again.
src/PirexRewards.sol: 84 85: function initialize() public initializer { 86: __Ownable_init(); 87: }
In the EVM, the constructor's job is actually to return the bytecode that will live at the contract's address. So, while inside a constructor, your address (address(this))
will be the deployment address, but there will be no bytecode at that address! So if we check address(this).code.length
before the constructor has finished, even from within a delegatecall, we will get 0. So now let's update our initialize()
function to only run if we are inside a constructor:
src/PirexRewards.sol: 84 85: function initialize() public initializer { + require(address(this).code.length == 0, 'not in constructor'); 86: __Ownable_init(); 87: }
Now the Proxy contract's constructor can still delegatecall initialize(), but if anyone attempts to call it again (after deployment) through the Proxy instance, or tries to call it directly on the above instance, it will revert because address(this).code.length will be nonzero.
Also, because we no longer need to write to any state to track whether initialize() has been called, we can avoid the 20k storage gas cost. In fact, the cost for checking our own code size is only 2 gas, which means we have a 10,000x gas savings over the standard version. Pretty neat!
Context: AutoPxGlp.sol#L24-L30
Recommendation: Storing state variables packaging is more gas efficient.
Current Code:
uint256 public withdrawalPenalty = 300; uint256 public platformFee = 1000; uint256 public compoundIncentive = 1000; address public platform;
Recommendation Code:
address public platform; uint64 public withdrawalPenalty = 300; uint64 public platformFee = 1000; uint64 public compoundIncentive = 1000;
Storage Variable | Slot | Offset | Length |
---|---|---|---|
platform | 0 | 0 | 20 |
withdrawalPenalty | 0 | 20 | 8 |
platformFee | 1 | 0 | 8 |
compoundIncentive | 1 | 8 | 8 |
assert
check in _computeAssetAmounts()
[19 K Gas Saved ]The assert
check in this function is not needed. Because this check will always be true because of this line: postFeeAmount = assets - feeAmount;
Deployment Gas Saved : 19 K
Transaction Gas Saved : 90
Context:
src/PirexGmx.sol: 216 */ 217: function _computeAssetAmounts(Fees f, uint256 assets) 218: internal 219: view 220: returns (uint256 postFeeAmount, uint256 feeAmount) 221: { 222: 223: 224: feeAmount = (assets * fees[f]) / FEE_DENOMINATOR; 225: postFeeAmount = assets - feeAmount; 226: 227: assert(feeAmount + postFeeAmount == assets); 228: }
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
contract GasTest is DSTest { Contract0 c0; Contract1 c1; function setUp() public { c0 = new Contract0(); c1 = new Contract1(); } function testGas() public { c0.depositGmx(5,200_000); c1.depositGmx(5,200_000); } } contract Contract0 { uint256 constant FEE_DENOMINATOR = 1_000_000; function depositGmx(uint256 amount, uint256 f) external { (uint256 postFeeAmount, uint256 feeAmount) = _computeAssetAmounts(f,amount); } function _computeAssetAmounts(uint f, uint256 assets) internal view returns (uint256 postFeeAmount, uint256 feeAmount) { feeAmount = (assets * f ) / FEE_DENOMINATOR; postFeeAmount = assets - feeAmount; assert(feeAmount + postFeeAmount == assets); } } contract Contract1 { uint256 constant FEE_DENOMINATOR = 1_000_000; function depositGmx(uint256 amount, uint256 f) external { (uint256 postFeeAmount, uint256 feeAmount) = _computeAssetAmounts(f,amount); } function _computeAssetAmounts(uint f, uint256 assets) internal view returns (uint256 postFeeAmount, uint256 feeAmount) { feeAmount = (assets * f ) / FEE_DENOMINATOR; postFeeAmount = assets - feeAmount; } }
Gas Report:
| src/test/test.sol:Contract0 contract | | | | | | |--------------------------------------|-----------------|-----|--------|-----|---------| | Deployment Cost | Deployment Size | | | | | | 102347 | 543 | | | | | | Function Name | min | avg | median | max | # calls | | depositGmx | 608 | 608 | 608 | 608 | 1 | | src/test/test.sol:Contract1 contract | | | | | | |--------------------------------------|-----------------|-----|--------|-----|---------| | Deployment Cost | Deployment Size | | | | | | 83329 | 448 | | | | | | Function Name | min | avg | median | max | # calls | | depositGmx | 518 | 518 | 518 | 518 | 1 |
assembly
to write address storage valuesContext:
src/PirexFees.sol: 50: constructor(address _treasury, address _contributors) Owned(msg.sender) { 54: treasury = _treasury; 55: contributors = _contributors; src/PirexGmx.sol: 167: constructor( 168: address _pxGmx, 169: address _pxGlp, 170: address _pirexFees, 171: address _pirexRewards, 172: address _delegateRegistry, 173: address _gmxBaseReward, 174: address _gmx, 175: address _esGmx, 176: address _gmxRewardRouterV2, 177: address _stakedGlp 193: pxGmx = PxERC20(_pxGmx); 194: pxGlp = PxERC20(_pxGlp); 195: pirexFees = PirexFees(_pirexFees); 196: pirexRewards = _pirexRewards; 197: delegateRegistry = DelegateRegistry(_delegateRegistry); 198: gmxBaseReward = ERC20(_gmxBaseReward); 199: gmx = ERC20(_gmx); 200: esGmx = ERC20(_esGmx); 201: gmxRewardRouterV2 = IRewardRouterV2(_gmxRewardRouterV2); 202: stakedGlp = IStakedGlp(_stakedGlp); src/PxERC20.sol: 24: constructor( 25: address _pirexRewards, 35: pirexRewards = PirexRewards(_pirexRewards); src/vaults/AutoPxGlp.sol: 66: constructor( 67: address _gmxBaseReward, 68: address _asset, 69: address _pxGmx, 70: string memory _name, 71: string memory _symbol, 72: address _platform, 73: address _rewardsModule 74: ) PxGmxReward(_pxGmx) PirexERC4626(ERC20(_asset), _name, _symbol) { 82: gmxBaseReward = ERC20(_gmxBaseReward); 83: platform = _platform; 84: rewardsModule = _rewardsModule; src/vaults/AutoPxGmx.sol: 73: constructor( 74: address _gmxBaseReward, 75: address _gmx, 76: address _asset, 77: string memory _name, 78: string memory _symbol, 79: address _platform, 80: address _rewardsModule 81: ) Owned(msg.sender) PirexERC4626(ERC20(_asset), _name, _symbol) { 90: gmxBaseReward = ERC20(_gmxBaseReward); 91: gmx = ERC20(_gmx); 92: platform = _platform; 93: rewardsModule = _rewardsModule; src/vaults/PirexERC4626.sol: 48: constructor( 49: ERC20 _asset, 53: asset = _asset; src/vaults/PxGmxReward.sol: 40: constructor(address _pxGmx) Owned(msg.sender) { 43: pxGmx = ERC20(_pxGmx);
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
contract GasTestFoundry is DSTest { Contract1 c1; Contract2 c2; function setUp() public { c1 = new Contract1(); c2 = new Contract2(); } function testGas() public { c1.setRewardTokenAndAmount(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,356); c2.setRewardTokenAndAmount(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,356); } } contract Contract1 { address rewardToken ; uint256 reward; function setRewardTokenAndAmount(address token_, uint256 reward_) external { rewardToken = token_; reward = reward_; } } contract Contract2 { address rewardToken ; uint256 reward; function setRewardTokenAndAmount(address token_, uint256 reward_) external { assembly { sstore(rewardToken.slot, token_) sstore(reward.slot, reward_) } } }
Gas Report:
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโฎ โ src/test/GasTest.t.sol:Contract1 contract โ โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโโโชโโโโโโโโโชโโโโโโโโชโโโโโโโโโโก โ Deployment Cost โ Deployment Size โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ 50899 โ 285 โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ Function Name โ min โ avg โ median โ max โ # calls โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ setRewardTokenAndAmount โ 44490 โ 44490 โ 44490 โ 44490 โ 1 โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโดโโโโโโโโดโโโโโโโโโโฏ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโฎ โ src/test/GasTest.t.sol:Contract2 contract โ โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโโโชโโโโโโโโโชโโโโโโโโชโโโโโโโโโโก โ Deployment Cost โ Deployment Size โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ 38087 โ 221 โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ Function Name โ min โ avg โ median โ max โ # calls โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ setRewardTokenAndAmount โ 44457 โ 44457 โ 44457 โ 44457 โ 1 โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโดโโโโโโโโดโโโโโโโโโโฏ
Description:
The project uses the onlyOwner
authorization model I recommend using Solady's highly gas optimized contract.
https://github.com/Vectorized/solady/blob/main/src/auth/OwnableRoles.sol
Description:
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check ofย msg.value == 0
ย and saves 13 gas
on deployment with no security risks.
Context:
8 results src/PirexFees.sol: 50: constructor(address _treasury, address _contributors) Owned(msg.sender) { src/PirexGmx.sol: 167: constructor( src/PxERC20.sol: 24: constructor( src/PxGmx.sol: 10: constructor(address _pirexRewards) src/vaults/AutoPxGlp.sol: 66: constructor( src/vaults/AutoPxGmx.sol: 73: constructor( src/vaults/PirexERC4626.sol: 48: constructor( src/vaults/PxGmxReward.sol: 40: constructor(address _pxGmx) Owned(msg.sender) {
Recommendation:
Set the constructor to payable
Proof Of Concept: https://forum.openzeppelin.com/t/a-collection-of-gas-optimisation-tricks/19966/5?u=pcaversaccio
The optimizer was turned on and set to 10000 runs
contract GasTestFoundry is DSTest { Contract1 c1; Contract2 c2; function setUp() public { c1 = new Contract1(); c2 = new Contract2(); } function testGas() public { c1.x(); c2.x(); } } contract Contract1 { uint256 public dummy; constructor() payable { dummy = 1; } function x() public { } } contract Contract2 { uint256 public dummy; constructor() { dummy = 1; } function x() public { } }
Gas Report:
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโฌโโโโโโโโโฌโโโโโโฌโโโโโโโโโโฎ โ src/test/GasTest.t.sol:Contract1 contract โ โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโชโโโโโโโโโชโโโโโโชโโโโโโโโโโก โ Deployment Cost โ Deployment Size โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโผโโโโโโโโโผโโโโโโผโโโโโโโโโโค โ 49563 โ 159 โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโผโโโโโโโโโผโโโโโโผโโโโโโโโโโค โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโฌโโโโโโโโโฌโโโโโโฌโโโโโโโโโโฎ โ src/test/GasTest.t.sol:Contract2 contract โ โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโชโโโโโโโโโชโโโโโโชโโโโโโโโโโก โ Deployment Cost โ Deployment Size โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโผโโโโโโโโโผโโโโโโผโโโโโโโโโโค โ 49587 โ 172 โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโผโโโโโโโโโผโโโโโโผโโโโโโโโโโค
payable
Context:ย
18 results - 5 files src/PirexFees.sol: 63: function setFeeRecipient(FeeRecipient f, address recipient) 64 external 65 onlyOwner 83: function setTreasuryFeePercent(uint8 _treasuryFeePercent) 84 external 85 onlyOwner src/PirexGmx.sol: 300: function setFee(Fees f, uint256 fee) external onlyOwner { 301 if (fee > FEE_MAX) revert InvalidFee(); 313: function setContract(Contracts c, address contractAddress) 314 external 315 onlyOwner 862: function setDelegationSpace( 863 string memory _delegationSpace, 864 bool shouldClear 865 ) external onlyOwner { 884: function setVoteDelegate(address voteDelegate) external onlyOwner { 909: function setPauseState(bool state) external onlyOwner { src/PirexRewards.sol: 93: function setProducer(address _producer) external onlyOwner { 432: function setRewardRecipientPrivileged( 433 address lpContract, 434 ERC20 producerToken, 435 ERC20 rewardToken, 436 address recipient 437 ) external onlyOwner { src/vaults/AutoPxGlp.sol: 94: function setWithdrawalPenalty(uint256 penalty) external onlyOwner { 106: function setPlatformFee(uint256 fee) external onlyOwner { 118: function setCompoundIncentive(uint256 incentive) external onlyOwner { 130: function setPlatform(address _platform) external onlyOwner { src/vaults/AutoPxGmx.sol: 104: function setPoolFee(uint24 _poolFee) external onlyOwner { 116: function setWithdrawalPenalty(uint256 penalty) external onlyOwner { 128: function setPlatformFee(uint256 fee) external onlyOwner { 140: function setCompoundIncentive(uint256 incentive) external onlyOwner { 152: function setPlatform(address _platform) external onlyOwner {
Description: If a function modifier or require such as onlyOwner-admin is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
Recommendation:
Functions guaranteed to revert when called by normal users can be marked payableย (for only onlyowner or admin
functions)
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
contract GasTest is DSTest { Contract0 c0; Contract1 c1; function setUp() public { c0 = new Contract0(); c1 = new Contract1(); } function testGas() public { c0.foo(); c1.foo(); } } contract Contract0 { uint256inverse; function foo() external { inverse++; } } contract Contract1 { uint256inverse; function foo() external payable { inverse++; } }
Gas Report:
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโฎ โ src/test/GasTest.t.sol:Contract0 contract โ ย ย ย ย ย ย ย ย โ ย ย ย โย ย ย ย โ ย ย ย โ ย ย ย ย โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโโโชโโโโโโโโโชโโโโโโโโชโโโโโโโโโโก โ Deployment Cost ย ย ย ย ย ย ย ย ย ย ย ย ย โ Deployment Size โ ย ย ย โย ย ย ย โ ย ย ย โ ย ย ย ย โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ 44293 ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย โ 252 ย ย ย ย ย ย โ ย ย ย โย ย ย ย โ ย ย ย โ ย ย ย ย โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ Function Name ย ย ย ย ย ย ย ย ย ย ย ย ย ย โ min ย ย ย ย ย ย โ avg ย โ median โ max ย โ # calls โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ foo ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย โ 22308 ย ย ย ย ย โ 22308 โ 22308ย โ 22308 โ 1 ย ย ย โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโดโโโโโโโโดโโโโโโโโโโฏ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโฎ โ src/test/GasTest.t.sol:Contract1 contract โ ย ย ย ย ย ย ย ย โ ย ย ย โย ย ย ย โ ย ย ย โ ย ย ย ย โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโโโชโโโโโโโโโชโโโโโโโโชโโโโโโโโโโก โ Deployment Cost ย ย ย ย ย ย ย ย ย ย ย ย ย โ Deployment Size โ ย ย ย โย ย ย ย โ ย ย ย โ ย ย ย ย โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ 41893 ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย โ 240 ย ย ย ย ย ย โ ย ย ย โย ย ย ย โ ย ย ย โ ย ย ย ย โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ Function Name ย ย ย ย ย ย ย ย ย ย ย ย ย ย โ min ย ย ย ย ย ย โ avg ย โ median โ max ย โ # calls โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโผโโโโโโโโโโค โ foo ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย โ 22284 ย ย ย ย ย โ 22284 โ 22284ย โ 22284 โ 1 ย ย ย โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโดโโโโโโโโดโโโโโโโโโโฏ
UUPS proxies are a lot simpler than Transparent, and they only have the logic for delegating the call. This makes them much cheaper to deploy, and each all has half the overhead (just a single SLOAD).
https://twitter.com/smpalladino/status/1389939166109212675?s=20&t=Nd7ssD9sC_BNTtQF8lnxNg
Context:ย All Contracts
Description:ย
Contracts most called functions could simply save gas by function ordering via Method ID
. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas
are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.ย
Recommendation:ย
Find a lower method ID
name for the most called functions for exampleย Call()ย vs.ย Call1()ย is cheaper by 22 gas
For example, the function IDs in the PirexGmx.sol
contract will be the most used; A lower method ID may be given.
Proof of Consept: https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92
PirexGmx.sol function names can be named and sorted according to METHOD ID
Sighash | Function Signature ======================== 57508589 => _calculateRewards(bool,bool) 59524400 => _computeAssetAmounts(Fees,uint256) 74874323 => setVoteDelegate(address) 492b669d => configureGmxState() 20587afb => setFee(Fees,uint256) dae84c59 => setContract(Contracts,address) 437a8d0a => depositGmx(uint256,address) f83e36e6 => depositFsGlp(uint256,address) d21fe1ee => _depositGlp(address,uint256,uint256,uint256,address) f64d0094 => depositGlpETH(uint256,uint256,address) c2ae96ef => depositGlp(address,uint256,uint256,uint256,address) a7fe1a7f => _redeemPxGlp(address,uint256,uint256,address) 6151f1b7 => redeemPxGlpETH(uint256,uint256,address) 414cc4ce => redeemPxGlp(address,uint256,uint256,address) 372500ab => claimRewards() 3e9f3619 => claimUserReward(address,uint256,address) 1d3c2b2f => setDelegationSpace(string,bool) edf187f0 => clearVoteDelegate() cdb88ad1 => setPauseState(bool) 71726c92 => initiateMigration(address) 24fcd907 => migrateReward() 6ac844be => completeMigration(address)
tokenAmount
and msg.value
check in the _depositGlp
functionChecking tokenAmount
and msg.value in _depositGlp
function if (tokenAmount == 0) revert ZeroAmount();
. done with, but it is not necessary
Because these are done in the interacted RewardRouterV2.sol
By removing this Require, approximately 3k gas is saved
RewardRouterV2.sol - mintAndStakeGlp RewardRouterV2.sol - mintAndStakeETH
src/PirexGmx.sol: 534 */ 535: function _depositGlp( 536: address token, 537: uint256 tokenAmount, 538: uint256 minUsdg, 539: uint256 minGlp, 540: address receiver 541: ) 542: internal 543: returns ( 544: uint256 deposited, 545: uint256 postFeeAmount, 546: uint256 feeAmount 547: ) 548: { 549: if (tokenAmount == 0) revert ZeroAmount();
v4.8.0 OpenZeppelin
contractsDescription: The upcoming v4.8.0 version of OpenZeppelin provides many small gas optimizations.
https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v4.8.0-rc.0
v4.8.0-rc.0 โฝย Many small optimizations
#0 - c4-judge
2022-12-05T14:16:08Z
Picodes marked the issue as grade-b
#1 - drahrealm
2022-12-09T06:03:31Z
Some of the considered tips already exist on previously confirmed findings while others are either invalid for the latest codebase, or minor relative to the added complexity + target deployment chains
#2 - c4-sponsor
2022-12-09T06:03:36Z
drahrealm marked the issue as sponsor disputed