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: 7/101
Findings: 5
Award: $3,824.26
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: cccz
Also found by: Englave, Jeiwan, aphak5010, hansfriese, immeas, rbserver, xiaoming90
82.2514 USDC - $82.25
https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L242 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L272
The AutoPxGmx.compound
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L242) function is used to compound the rewards from the pxGmx tokens.
The gmxBaseReward
must therefore be swapped for gmx
tokens.
This is done by calling the IV3SwapRouter.exactInputSingle
function.
The AutoPxGmx.compound
function takes a fee
paramter that is then passed to the IV3SwapRouter
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L272).
The fee
parameter can be any uint24
number.
When the compound
function is called internally, the fee is 3000
which the IV3SwapRouter
interprets as 0.3%
.
As you can see here, there can be different pools with different fees for a token pair. Also more fee levels can be introduced in the future.
Uniswap v3 introduces multiple pools for each token pair, each with a different swapping fee. Liquidity providers may initially create pools at three fee levels: 0.05%, 0.30%, and 1%. More fee levels may be added by UNI governance
This opens up the possibility of the compound
function to be called with an unnecessarily high fee
parameter, thereby causing a loss for aPxGmx
token holders.
Just look at the compound
function and see that the fee
parameter is passed to the IV3SwapRouter
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L272).
VSCode
There exists a poolFee
variable in the AutoPxGmx
contract that can be adapted by the owner
via the setPoolFee
function. The poolFee
variable is currently only used when the compound
function is called internally.
The fee
paramter could be removed from the compound
function and instead the poolFee
variable can be used.
Alternatively you might use a MAX_POOL_FEE
variable and verify that the fee
is not bigger than the MAX_POOL_FEE
. Thereby the compound
function might use 0.3%
pools and 1%
pools but there is no possibility for large fees if Uniswap introduces pools with higher fees.
#0 - c4-judge
2022-12-04T00:18:27Z
Picodes marked the issue as duplicate of #391
#1 - c4-judge
2022-12-05T10:32:53Z
Picodes marked the issue as duplicate of #91
#2 - Picodes
2023-01-01T11:06:23Z
Partial credit as this issue does not highlight the main risk which is the use of a highly illiquid pool
#3 - c4-judge
2023-01-01T11:06:27Z
Picodes marked the issue as partial-50
🌟 Selected for report: aphak5010
3576.9207 USDC - $3,576.92
https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/PirexERC4626.sol#L225 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L14 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGlp.sol#L14 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L315 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L199 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L215-L216 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L332
The ERC-4626 Tokenized Vault Standard requires the maxWithdraw
function to be implemented (https://ethereum.org/en/developers/docs/standards/tokens/erc-4626/#maxwithdraw).
This function is supposed to return "the maximum amount of underlying assets that can be withdrawn from the owner balance with a single withdraw call".
The PirexERC4626
contract implements this function (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/PirexERC4626.sol#L225).
It is implemented correctly when PirexERC4626
is used on its own.
However in this project, the PirexERC4626
contract is not used on its own but inherited by AutoPxGmx
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L14) and AutoPxGlp
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGlp.sol#L14).
AutoPxGmx
and AutoPxGlp
implement a withdrawalPenalty
i.e. a fee that is paid when a user wants to withdraw assets from the vault.
AutoPxGmx
and AutoPxGlp
do not override the maxWithdraw
function.
This causes the maxWithdraw
function to return an amount of assets that is too big to be withdrawn.
So when maxWithdraw
is called and with the returned amount withdraw
is called, the call to withdraw
will revert.
This can cause issues in any upstream components that rely on AutoPxGmx
and AutoPxGlp
to correctly implement the ERC4626 standard.
For example an upstream wrapper might only allow withdrawals with the maximum amount and determine this maximum amount by calling the maxWithdraw
function. As this function returns a value that is too big, no withdrawals will be possible.
maxWithdraw
function in a AutoPxGmx
contract is calledwithdraw
function is called with the value that was returned by the maxWithdraw
function (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L315)withdraw
function in turn calls the previewWithdraw
function (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L199)previewWithdraw
function will increase the amount of shares to include the withdrawalPenalty
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L215-L216) which causes the amount of shares to burn to be too large and the call to burn
will revert (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L332)VSCode
In the AutoPxGmx
and AutoPxGlp
function implement the maxWithdraw
function that overrides the function in PirexERC4626
and takes into account the withdrawalPenalty
.
Potential fix:
function maxWithdraw(address owner) public view override returns (uint256) { uint256 shares = balanceOf(owner); // Calculate assets based on a user's % ownership of vault shares uint256 assets = convertToAssets(shares); uint256 _totalSupply = totalSupply; // Calculate a penalty - zero if user is the last to withdraw uint256 penalty = (_totalSupply == 0 || _totalSupply - shares == 0) ? 0 : assets.mulDivDown(withdrawalPenalty, FEE_DENOMINATOR); // Redeemable amount is the post-penalty amount return assets - penalty; }
#0 - c4-sponsor
2022-12-08T05:13:06Z
drahrealm marked the issue as sponsor confirmed
#1 - c4-judge
2022-12-30T21:12:23Z
Picodes marked the issue as satisfactory
🌟 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/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L97 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L370 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L242 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L152 https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGlp.sol#L87
The AutoPxGmx
contract calls in its constructor gmx.safeApprove(_platform, type(uint256).max);
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L97).
This means that the platform
can now transfer gmx
token that is owned by AutoPxGmx
.
This is needed in order for the AutoPxGmx.depositGmx
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L370) and AutoPxGmx.compound
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L242) functions to work since by calling any of them the platform
will transfer the gmx
tokens to be deposited from the AutoPxGmx
contract.
The issue is this:
The AutoPxGmx
contract has a function called setPlatform
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGmx.sol#L152) that allows to change the platform
variable.
This function does NOT approve the new platform
to transfer gmx
tokens.
So effectively the platform
cannot be changed since changing it makes the contract unable to function properly.
The owner
of the AutoPxGmx
contract would of course notice the issue and can change the platform
to be the original again.
But the issue remains that the platform
must stay the same in order for the contract to be fully operable.
The same issue exists for the AutoPxGlp
contract and the gmxBaseReward
token (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/vaults/AutoPxGlp.sol#L87).
I won't go into this in detail since the issue is pretty much the same. You can see how to fix both issues in the "Recommend Mitigation Steps" section.
AutoPxGmx
contract is set upAutoPxGmx.setPlatform
function is called in order to change the platform
AutoPxGmx.depositGmx
or AutoPxGmx.compound
is going to fail because the platform
is missing approval to transfer gmx
tokens owned by AutoPxGmx
VSCode
Change AutoPxGmx.setPlatform
function like this:
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); }
And change AutoPxGlp.setPlatform
function like this:
function setPlatform(address _platform) external onlyOwner { if (_platform == address(0)) revert ZeroAddress(); + gmxBaseReward.safeApprove(address(platform), 0); platform = _platform; + gmxBaseReward.safeApprove(address(_platform), type(uint256).max); emit PlatformUpdated(_platform); }
#0 - c4-judge
2022-12-04T12:31:02Z
Picodes marked the issue as duplicate of #182
#1 - c4-judge
2023-01-01T11:11:38Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-01-01T11:29:54Z
Picodes changed the severity to 3 (High Risk)
#3 - c4-judge
2023-01-01T11:32:49Z
Picodes changed the severity to 2 (Med Risk)
🌟 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
Risk | Title | File | Instances |
---|---|---|---|
L-00 | No zero address check and 2-Step-Transfer in solmate/auth/Owned.sol | - | 4 |
L-01 | Missing whenNotPaused modifier | PirexGmx.sol | 2 |
L-02 | Unsafe check that addres is a contract | PirexRewards.sol | 1 |
N-00 | Event is missing indexed fields | - | 34 |
N-01 | Variable can be declared immutable | PxGmxReward.sol | 1 |
N-02 | Comment not according to logic | - | 3 |
4 contracts inherit the Owned
contract in solmate/auth/Owned.sol
(https://github.com/transmissions11/solmate/blob/main/src/auth/Owned.sol).
Inheriting contracts:
This Owned
contract is a very minimal implementation and allows the owner of the contract to assign any new owner (even the zero address) (https://github.com/transmissions11/solmate/blob/8d910d876f51c3b2585c9109409d601f600e68e1/src/auth/Owned.sol#L40).
It is best practice to add a zero-address check and make transferring ownership a 2-Step-Process.
The current owner sets a pending owner and the pending owner must then claim ownership.
This mitigates scenarios where ownership of the contract can be lost.
whenNotPaused
modifierThe PirexGmx.claimUserReward
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexGmx.sol#L824) and PirexGmx.claimRewards
(https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexGmx.sol#L739) functions should include the whenNotPaused
modifier meaning they should only be executed when the contract is not paused.
This is because in a paused state important gmx state variables can be configured with the PirexGmx.configureGmxState
function (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexGmx.sol#L272) and the contract can be migrated.
So one must assume that when the contract is paused, the gmx state variables might not be assigned correctly or the contract is undergoing migration.
However the claimUserReward
and claimRewards
functions rely on the state to be configured correctly. So they should only be callable when the contract is not paused.
In the worst case that the old gmx state behaves very badly, a loss of funds might occur by calling the claimRewards
or claimUserReward
function.
The PirexRewards.setRewardRecipientPrivileged
and PirexRewards.unsertRewardRecipientPrivileged
functions check that the lpContract
parameter is the address of a contract by checking the code.length
property.
This is an unsafe way to check if the address is the address of a contract because a contract can temporarily sefldestruct and be recreated by using CREATE2
.
This means that a contract can be set using the setRewardRecipientPrivileged
function but cannot be unsert anymore using the unsetRewardRecipientPrivileged
function if the contract uses selfdestruct
.
In the function documentation it is mentioned that the functions will only be used to handle Pirex-GMX
LP contracts (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexRewards.sol#L424-L427). So there should hopefuilly be no issue as long as the functions are used as intended.
However I think this information might still be helpful to you.
Each event should use three indexed fields if it has three or more fields.
There are 34 instances of events that do not have 3 indexed fields.
immutable
Variables that are only assigned in the constructor should be declared immutable
.
There are 1 instances of this.
The above 2 comments state that the transfer
/ transferFrom
function is overriden in order to allow a pre-transfer
internal hook to run.
However when you look into the function code you will see that it is not a pre-transfer
but post-transfer
hook that runs.
It is not the "Uniswap V3 router" that is approved but the platform.
#0 - c4-judge
2022-12-05T09:49:23Z
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
39.6537 USDC - $39.65
The Gas savings are calculated using the scripts/forgeTest.sh --gas-report
command.
The same tests were used that are provided in the contest repository.
Issue | Title | File | Instances | Estimated Gas Savings (Deployments) | Estimated Gas Savings (Method calls) |
---|---|---|---|---|---|
G-00 | Claiming rewards from PirexRewards requires redundant calls to harvest function | PirexRewards.sol | 1 | not calculated | not calculated |
G-01 | Superfluous assert statement | PirexGmx.sol | 1 | 9216 | 95 |
G-02 | X = X + Y costs less Gas than X += Y | PxGmxReward.sol | 1 | 1200 | 1714 |
PirexRewards
requires redundant calls to harvest
functionA user can claim rewards from the PirexRewards
contract with the claim
function (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexRewards.sol#L373).
The claim
function calls the harvest
function which claims rewards from the producer
for multiple producerToken
s (https://github.com/code-423n4/2022-11-redactedcartel/blob/03b71a8d395c02324cb9fdaf92401357da5b19d1/src/PirexRewards.sol#L346-L347).
So when a user wants to claim
rewards from the PirexRewards
contract for multiple producerToken
s, he needs to call the claim
function once for each producerToken
which results in multiple of the same calls to producer.claimRewards
.
One call to producer.claimRewards
is enough.
This can be achieved by implementing a claimBatch
function in PirexRewards
that calls harvest
and thereby producer.claimRewards
once and then iterates over all provided producerToken
s.
This results in substantial Gas savings since one call to producer.claimRewards
requires on average 161614
Gas.
assert
statementThe assert
statement is redundant since it will always be true.
This is because the line above it calculates:
postFeeAmount = assets - feeAmount;
By applying basic algebra it follows that:
assets = postFeeAmount + feeAmount
This is exactly what the assert
checks. But as you can see the check will always be true.
So the assert
statement can just be removed.
Deployment Gas saved: 9216
Method call Gas saved: 95
X = X + Y
costs less Gas than X += Y
There is 1 instance of this:
Fix:
rewardState = rewardState + rewardAmount;
Deployment Gas saved: 1200
Method call Gas saved: 1714
#0 - c4-judge
2022-12-05T14:23:29Z
Picodes marked the issue as grade-b
#1 - Picodes
2022-12-05T14:23:39Z
G-00 could be interesting
#2 - c4-sponsor
2022-12-09T05:47:42Z
drahrealm marked the issue as sponsor confirmed