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: 23/101
Findings: 1
Award: $604.66
๐ Selected for report: 0
๐ Solo Findings: 0
๐ 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
NB: Some functions have been truncated where neccessary to just show affected parts of the code Throught the report some places might be denoted with audit tags to show the actual place affected.
Use immutable if you want to assign a permanent value at construction. Use constants if you already know the permanent value. Both get directly embedded in bytecode, saving SLOAD. Variables only set in the constructor and never edited afterwards should be marked as immutable, as it would avoid the expensive storage-writing operation in the constructor (around 20 000 gas per variable) and replace the expensive storage-reading operations (around 2100 gas per reading) to a less expensive value reading (3 gas)
File: /src/vaults/PxGmxReward.sol 15: ERC20 public pxGmx;
diff --git a/src/vaults/PxGmxReward.sol b/src/vaults/PxGmxReward.sol index ec892e7..9c540b3 100644 --- a/src/vaults/PxGmxReward.sol +++ b/src/vaults/PxGmxReward.sol @@ -12,7 +12,7 @@ contract PxGmxReward is Owned { using SafeTransferLib for ERC20; using SafeCastLib for uint256; - ERC20 public pxGmx; + ERC20 public immutable pxGmx; GlobalState public globalState; uint256 public rewardState;
Here, the storage variables can be tightly packed from:
File: /src/vaults/AutoPxGmx.sol //@audit: Move uint24 public poolFee next to address public platform as they can be packed together 25: // Uniswap pool fee 26: uint24 public poolFee = 3000; 28: uint256 public withdrawalPenalty = 300; 29: uint256 public platformFee = 1000; 30: uint256 public compoundIncentive = 1000; 31: address public platform;
diff --git a/src/vaults/AutoPxGmx.sol b/src/vaults/AutoPxGmx.sol index 69d1b85..0466629 100644 --- a/src/vaults/AutoPxGmx.sol +++ b/src/vaults/AutoPxGmx.sol @@ -22,12 +22,13 @@ contract AutoPxGmx is ReentrancyGuard, Owned, PirexERC4626 { uint256 public constant FEE_DENOMINATOR = 10000; uint256 public constant MAX_COMPOUND_INCENTIVE = 5000; - // Uniswap pool fee - uint24 public poolFee = 3000; + uint256 public withdrawalPenalty = 300; uint256 public platformFee = 1000; uint256 public compoundIncentive = 1000; + // Uniswap pool fee + uint24 public poolFee = 3000; address public platform; // Address of the rewards module (ie. PirexRewards instance)
File: /src/vaults/AutoPxGmx.sol 370: function depositGmx(uint256 amount, address receiver) 379: if (totalAssets() != 0) beforeDeposit(address(0), 0, 0); 381: // Intake sender GMX 382: gmx.safeTransferFrom(msg.sender, address(this), amount); 394: if ( 395: (shares = supply == 0 396: ? assets 397: : assets.mulDivDown(supply, totalAssets() - assets)) == 0 398: ) revert ZeroShares();
diff --git a/src/vaults/AutoPxGmx.sol b/src/vaults/AutoPxGmx.sol index 69d1b85..1c55d24 100644 --- a/src/vaults/AutoPxGmx.sol +++ b/src/vaults/AutoPxGmx.sol @@ -376,7 +376,8 @@ contract AutoPxGmx is ReentrancyGuard, Owned, PirexERC4626 { 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); + uint256 _totalAssets = totalAssets(); + if (_totalAssets != 0) beforeDeposit(address(0), 0, 0); // Intake sender GMX gmx.safeTransferFrom(msg.sender, address(this), amount); @@ -394,7 +395,7 @@ contract AutoPxGmx is ReentrancyGuard, Owned, PirexERC4626 { if ( (shares = supply == 0 ? assets - : assets.mulDivDown(supply, totalAssets() - assets)) == 0 + : assets.mulDivDown(supply, _totalAssets - assets)) == 0 ) revert ZeroShares(); _mint(receiver, shares);
When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct
File: /src/PirexRewards.sol 216: UserState memory userState = producerTokens[producerToken].userStates[ 217: user 218: ];
diff --git a/src/PirexRewards.sol b/src/PirexRewards.sol index 59c5ff7..76874de 100644 --- a/src/PirexRewards.sol +++ b/src/PirexRewards.sol @@ -213,7 +213,7 @@ contract PirexRewards is OwnableUpgradeable { uint256 rewards ) { - UserState memory userState = producerTokens[producerToken].userStates[ + UserState storage userState = producerTokens[producerToken].userStates[ user ];
A similar suggestion to the above has already been implemented on Line 285
285: UserState storage u = producerTokens[producerToken].userStates[user];
File: /src/vaults/PxGmxReward.sol 95: rewardState += rewardAmount;
diff --git a/src/vaults/PxGmxReward.sol b/src/vaults/PxGmxReward.sol index ec892e7..a52f749 100644 --- a/src/vaults/PxGmxReward.sol +++ b/src/vaults/PxGmxReward.sol @@ -92,7 +92,7 @@ contract PxGmxReward is Owned { _globalAccrue(); if (rewardAmount != 0) { - rewardState += rewardAmount; + rewardState = rewardState + rewardAmount; emit Harvest(rewardAmount); }
When using elements that are smaller than 32 bytes, your contractโs gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Use a larger size then downcast where needed
File: /src/vaults/AutoPxGmx.sol //@audit: uint24 _poolFee 104: function setPoolFee(uint24 _poolFee) external onlyOwner { //@audit: uint24 fee,uint160 sqrtPriceLimitX96 242: function compound( 243: uint24 fee, 244: uint256 amountOutMinimum, 245: uint160 sqrtPriceLimitX96, 246: bool optOutIncentive 247: )
File: /src/PirexFees.sol //@audit: uint8 _treasuryFeePercent 83: function setTreasuryFeePercent(uint8 _treasuryFeePercent)
The majority of Solidity for loops increment a uint256 variable that starts at 0. These increment operations never need to be checked for over/underflow because the variable will never reach the max number of uint256 (will run out of gas long before that happens). The default over/underflow check wastes gas in every iteration of virtually every for loop . eg.
e.g Let's work with a sample loop below.
for(uint256 i; i < 10; i++){ //doSomething }
can be written as shown below.
for(uint256 i; i < 10;) { // loop logic unchecked { i++; } }
We can also write it as an inlined function like below.
function inc(i) internal pure returns (uint256) { unchecked { return i + 1; } } for(uint256 i; i < 10; i = inc(i)) { // doSomething }
File: /src/PirexRewards.sol 163: for (uint256 i; i < len; ++i) { 164: if (address(rewardTokens[i]) == address(rewardToken)) { 165: revert TokenAlreadyAdded(); 166: } 167: }
diff --git a/src/PirexRewards.sol b/src/PirexRewards.sol index 59c5ff7..41da35e 100644 --- a/src/PirexRewards.sol +++ b/src/PirexRewards.sol @@ -160,10 +160,13 @@ contract PirexRewards is OwnableUpgradeable { ERC20[] memory rewardTokens = p.rewardTokens; uint256 len = rewardTokens.length; - for (uint256 i; i < len; ++i) { + for (uint256 i; i < len;) { if (address(rewardTokens[i]) == address(rewardToken)) { revert TokenAlreadyAdded(); } + unchecked { + ++i; + } }
File:/src/PirexRewards.sol 351: for (uint256 i; i < pLen; ++i) { 363: }
diff --git a/src/PirexRewards.sol b/src/PirexRewards.sol index 59c5ff7..2fc8bb9 100644 --- a/src/PirexRewards.sol +++ b/src/PirexRewards.sol @@ -348,7 +348,7 @@ contract PirexRewards is OwnableUpgradeable { uint256 pLen = _producerTokens.length; // Iterate over the producer tokens and update reward state - for (uint256 i; i < pLen; ++i) { + for (uint256 i; i < pLen;) { ERC20 p = _producerTokens[i]; uint256 r = rewardAmounts[i]; @@ -360,6 +360,9 @@ contract PirexRewards is OwnableUpgradeable { if (r != 0) { producerState.rewardStates[rewardTokens[i]] += r; } + unchecked { + ++i; + } }
File: /src/PirexRewards.sol 396: for (uint256 i; i < rLen; ++i) { 415: }
diff --git a/src/PirexRewards.sol b/src/PirexRewards.sol index 59c5ff7..56e5d91 100644 --- a/src/PirexRewards.sol +++ b/src/PirexRewards.sol @@ -393,7 +393,7 @@ contract PirexRewards is OwnableUpgradeable { emit Claim(producerToken, user); // Transfer the proportionate reward token amounts to the recipient - for (uint256 i; i < rLen; ++i) { + for (uint256 i; i < rLen;) { ERC20 rewardToken = rewardTokens[i]; address rewardRecipient = p.rewardRecipients[user][rewardToken]; address recipient = rewardRecipient != address(0) @@ -412,6 +412,9 @@ contract PirexRewards is OwnableUpgradeable { recipient ); } + unchecked { + ++i; + } } }
payable
save around 20 gas per function callIf a function modifier such as onlyOwner
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.
File:/src/PirexFees.sol 63: function setFeeRecipient(FeeRecipient f, address recipient) 64: external 65: onlyOwner 66: { 83: function setTreasuryFeePercent(uint8 _treasuryFeePercent) 84: external 85: onlyOwner 96: {
File: /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 {
File: /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 {
File: /src/PirexRewards.sol 93: function setProducer(address _producer) external onlyOwner { 151: function addRewardToken(ERC20 producerToken, ERC20 rewardToken) 152: external 153: onlyOwner 154: { 179: function removeRewardToken(ERC20 producerToken, uint256 removalIndex) 180: external 181: onlyOwner 182: { 432: function setRewardRecipientPrivileged( 433: address lpContract, 434: ERC20 producerToken, 435: ERC20 rewardToken, 436: address recipient 437: ) external onlyOwner { 461: function unsetRewardRecipientPrivileged( 462: address lpContract, 463: ERC20 producerToken, 464: ERC20 rewardToken 465: ) external onlyOwner {
File: /src/PirexGmx.sol 300: function setFee(Fees f, uint256 fee) external onlyOwner { 313: function setContract(Contracts c, address contractAddress) 314: external 315: onlyOwner 316: { 862: function setDelegationSpace( 863: string memory _delegationSpace, 864: bool shouldClear 865: ) external onlyOwner { 884: function setVoteDelegate(address voteDelegate) external onlyOwner { 895: function clearVoteDelegate() public onlyOwner { 909: function setPauseState(bool state) external onlyOwner { 921: function initiateMigration(address newContract) 922: external 923: whenPaused 924: onlyOwner 925: { 956: function completeMigration(address oldContract) 957: external 958: whenPaused 959: onlyOwner 960: {
The code can be optimized by minimizing the number of SLOADs.
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
File: /src/PirexGmx.sol 272: function configureGmxState() external onlyOwner whenPaused { 274: rewardTrackerGmx = RewardTracker(gmxRewardRouterV2.feeGmxTracker()); 275: rewardTrackerGlp = RewardTracker(gmxRewardRouterV2.feeGlpTracker()); 276: feeStakedGlp = RewardTracker(gmxRewardRouterV2.stakedGlpTracker()); 277: stakedGmx = RewardTracker(gmxRewardRouterV2.stakedGmxTracker()); 278: glpManager = gmxRewardRouterV2.glpManager(); 279: gmxVault = IVault(IGlpManager(glpManager).vault());
diff --git a/src/PirexGmx.sol b/src/PirexGmx.sol index e0034a1..0ff0bef 100644 --- a/src/PirexGmx.sol +++ b/src/PirexGmx.sol @@ -271,11 +271,12 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { */ function configureGmxState() external onlyOwner whenPaused { // Variables which can be assigned by reading previously-set GMX contracts - rewardTrackerGmx = RewardTracker(gmxRewardRouterV2.feeGmxTracker()); - rewardTrackerGlp = RewardTracker(gmxRewardRouterV2.feeGlpTracker()); - feeStakedGlp = RewardTracker(gmxRewardRouterV2.stakedGlpTracker()); - stakedGmx = RewardTracker(gmxRewardRouterV2.stakedGmxTracker()); - glpManager = gmxRewardRouterV2.glpManager(); + IRewardRouterV2 _gmxRewardRouterV2 = gmxRewardRouterV2; + rewardTrackerGmx = RewardTracker(_gmxRewardRouterV2.feeGmxTracker()); + rewardTrackerGlp = RewardTracker(_gmxRewardRouterV2.feeGlpTracker()); + feeStakedGlp = RewardTracker(_gmxRewardRouterV2.stakedGlpTracker()); + stakedGmx = RewardTracker(_gmxRewardRouterV2.stakedGmxTracker()); + glpManager = _gmxRewardRouterV2.glpManager(); gmxVault = IVault(IGlpManager(glpManager).vault());
File: /src/PirexGmx.sol 272: function configureGmxState() external onlyOwner whenPaused { 279: gmxVault = IVault(IGlpManager(glpManager).vault()); 281: emit ConfigureGmxState( 282: msg.sender, 283: rewardTrackerGmx, 284: rewardTrackerGlp, 285: feeStakedGlp, 286: stakedGmx, 287: glpManager, 288: gmxVault 289: );
diff --git a/src/PirexGmx.sol b/src/PirexGmx.sol index e0034a1..06a1e84 100644 --- a/src/PirexGmx.sol +++ b/src/PirexGmx.sol @@ -276,7 +276,8 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { feeStakedGlp = RewardTracker(gmxRewardRouterV2.stakedGlpTracker()); stakedGmx = RewardTracker(gmxRewardRouterV2.stakedGmxTracker()); glpManager = gmxRewardRouterV2.glpManager(); - gmxVault = IVault(IGlpManager(glpManager).vault()); + address _glpManager = glpManager; + gmxVault = IVault(IGlpManager(_glpManager).vault()); emit ConfigureGmxState( msg.sender, @@ -284,7 +285,7 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { rewardTrackerGlp, feeStakedGlp, stakedGmx, - glpManager, + _glpManager, gmxVault );
File: /src/PirexGmx.sol 272: function configureGmxState() external onlyOwner whenPaused { 277: stakedGmx = RewardTracker(gmxRewardRouterV2.stakedGmxTracker()); 281: emit ConfigureGmxState( 282: msg.sender, 283: rewardTrackerGmx, 284: rewardTrackerGlp, 285: feeStakedGlp, 286: stakedGmx, 287: glpManager, 288: gmxVault 289: ); 292: gmx.safeApprove(address(stakedGmx), type(uint256).max); 293: }
diff --git a/src/PirexGmx.sol b/src/PirexGmx.sol index e0034a1..4035b4a 100644 --- a/src/PirexGmx.sol +++ b/src/PirexGmx.sol @@ -278,18 +278,20 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { glpManager = gmxRewardRouterV2.glpManager(); gmxVault = IVault(IGlpManager(glpManager).vault()); + RewardTracker _stakedGmx = stakedGmx; + emit ConfigureGmxState( msg.sender, rewardTrackerGmx, rewardTrackerGlp, feeStakedGlp, - stakedGmx, + _stakedGmx, glpManager, gmxVault ); // Approve GMX to enable staking - gmx.safeApprove(address(stakedGmx), type(uint256).max); + gmx.safeApprove(address(_stakedGmx), type(uint256).max); }
File: /src/PirexGmx.sol 422: function depositFsGlp(uint256 amount, address receiver) 436: stakedGlp.transferFrom(msg.sender, address(this), amount); 452: emit DepositGlp( 453: msg.sender, 454: receiver, 455: address(stakedGlp), ... 462: );
diff --git a/src/PirexGmx.sol b/src/PirexGmx.sol index e0034a1..78f5324 100644 --- a/src/PirexGmx.sol +++ b/src/PirexGmx.sol @@ -433,7 +433,8 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { if (receiver == address(0)) revert ZeroAddress(); // Transfer the caller's fsGLP (unstaked for the user, staked for this contract) - stakedGlp.transferFrom(msg.sender, address(this), amount); + IStakedGlp _stakedGlp = stakedGlp; + _stakedGlp.transferFrom(msg.sender, address(this), amount); // Get the pxGLP amounts for the receiver and the protocol (fees) (uint256 postFeeAmount, uint256 feeAmount) = _computeAssetAmounts( @@ -452,7 +453,7 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { emit DepositGlp( msg.sender, receiver, - address(stakedGlp), + address(_stakedGlp), 0, 0, 0,
File: /src/PirexGmx.sol 739: function claimRewards() 763: uint256 esGmxBeforeClaim = stakedGmx.depositBalances( 764: address(this), 765: address(esGmx) 766: ); 787: uint256 esGmxRewards = stakedGmx.depositBalances( 788: address(this), 789: address(esGmx) 790: ) - esGmxBeforeClaim;
diff --git a/src/PirexGmx.sol b/src/PirexGmx.sol index e0034a1..d3e8754 100644 --- a/src/PirexGmx.sol +++ b/src/PirexGmx.sol @@ -760,7 +760,8 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { // Get pre-reward claim reward token balances to calculate actual amount received uint256 baseRewardBeforeClaim = gmxBaseReward.balanceOf(address(this)); - uint256 esGmxBeforeClaim = stakedGmx.depositBalances( + RewardTracker _stakedGmx = stakedGmx; + uint256 esGmxBeforeClaim = _stakedGmx.depositBalances( address(this), address(esGmx) ); @@ -784,7 +785,7 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { uint256 baseRewards = gmxBaseReward.balanceOf(address(this)) - baseRewardBeforeClaim; - uint256 esGmxRewards = stakedGmx.depositBalances( + uint256 esGmxRewards = _stakedGmx.depositBalances( address(this), address(esGmx) ) - esGmxBeforeClaim;
File: /src/PirexGmx.sol 940: function migrateReward() external whenPaused { 941: if (msg.sender != migratedTo) revert NotMigratedTo(); 946: gmxBaseReward.safeTransfer( 947: migratedTo, 948: gmxBaseReward.balanceOf(address(this)) 949: ); 950: }
diff --git a/src/PirexGmx.sol b/src/PirexGmx.sol index e0034a1..e7b4a1c 100644 --- a/src/PirexGmx.sol +++ b/src/PirexGmx.sol @@ -938,13 +938,14 @@ contract PirexGmx is ReentrancyGuard, Owned, Pausable { @notice Migrate remaining (base) reward to the new contract after completing migration */ function migrateReward() external whenPaused { - if (msg.sender != migratedTo) revert NotMigratedTo(); + address _migratedTo = migratedTo; + if (msg.sender != _migratedTo) revert NotMigratedTo(); if (gmxRewardRouterV2.pendingReceivers(address(this)) != address(0)) revert PendingMigration(); // Transfer out any remaining base reward (ie. WETH) to the new contract gmxBaseReward.safeTransfer( - migratedTo, + _migratedTo, gmxBaseReward.balanceOf(address(this)) ); }
#0 - c4-judge
2022-12-05T11:07:27Z
Picodes marked the issue as grade-a