Platform: Code4rena
Start Date: 28/04/2022
Pot Size: $50,000 USDC
Total HM: 7
Participants: 43
Period: 5 days
Judge: gzeon
Total Solo HM: 2
Id: 115
League: ETH
Rank: 1/43
Findings: 4
Award: $14,334.19
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: 0xDjango
13820.2885 USDC - $13,820.29
https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/liquidityMining/v2/PARMinerV2.sol#L126 https://github.com/Uniswap/v2-periphery/blob/2efa12e0f2d808d9b49737927f0e416fafa5af68/contracts/UniswapV2Router02.sol#L299 https://github.com/Uniswap/solidity-lib/blob/c01640b0f0f1d8a85cba8de378cc48469fcfd9a6/contracts/libraries/TransferHelper.sol#L47-L50
A malicious user is able to steal all collateral of an unhealthy position in PARMinerV2.sol
. The code for the liquidate()
function is written so that the following steps are followed:
PARMinerV2.liquidate()
_a.parallel().core().liquidatePartial()
PARMinerV2.liquidate()
checks that PARMinerV2's PAR balance is higher than the balance at the beginning of the function call.The exploit occurs with the arbitrary router call. The malicious user is able to supply the dexTxnData
parameter which dictates the function call to the router. If the user supplied a function such as UniswapV2Router's swapExactTokenForETH()
, then control flow will be given to the user, allowing them to perform the exploit.
Note: The Mimo developers have stated that the routers used by the protocol will be DEX Aggregators such as 1inch and Paraswap, but this submission will be referring to UniswapV2Router for simplicity. It can be assumed that the dex aggregators currently allow swapping tokens for ETH.
Continuing the exploit, once the attacker has gained control due to the ETH transfer, they are able to swap the ETH for PAR. Finally, they deposit the PAR with PARMinerV2.deposit()
. This will cause the final check of liquidate()
to pass because PARMinerV2's PAR balance will be larger than the start of the liquidation call.
The attacker is able to steal all collateral from every unhealthy position that they liquidate. In the most extreme case, the attacker is able to open their own risky positions with the hope that the position becomes unhealthy. They will borrow the PAR and then liquidate themselves to take back the collateral. Thus effectively stealing PAR.
Steps for exploit:
PARMinerV2.liquidate()
PARMinerV2
liquidate()
function, attacker supplies bytes for UniswapV2Router.swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
. For to
, they supply the attacker contract.swapExactTokensForETH()
firstly swaps the collateral for ETH and then transfers the ETH to the user with TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
TransferHelper.safeTransferETH()
contains a call to the receiver via (bool success, ) = to.call{value: value}(new bytes(0));
The attacker contract will then perform the following steps:
PARMinerV2
Static review.
The arbitrary call to the router contracts is risky because of the various functions that they can contain. Perhaps a solution is to only allow certain calls such as swapping tokens to tokens, not ETH. This would require frequently updated knowledge of the router's functions, though would be beneficial for security.
Also, adding a check that the _totalStake
variable has not increased during the liquidation call will mitigate the risk of the attacker depositing the PAR to increase the contract's balance. The attacker would have no option but to transfer the PAR to PARMinerV2 as is intended.
#0 - m19
2022-05-05T09:05:06Z
We believe in theory this attack is actually possible, but highly unlikely to happen. It also begs the question of whether it's really worth it for an attacker to do this because they could just call VaultsCore.liquidate() themselves (for example with a flashloan) and stake all the PAR they profit that way directly.
#1 - m19
2022-05-07T07:58:07Z
We misunderstood this exploit wrong and we confirm it. Basically, if the attacker was liquidating a 10,000 PAR position, he could potentially end up with a 10,000 PAR stake + liquidation profits. Our previous understanding was that he could only end up with the profits.
At the very least we'll implement a check that totalStake
hasn't changed, we will carefully consider if more changes are needed.
🌟 Selected for report: hyh
Also found by: 0xDjango, berndartmueller, cccz, defsec, delfin454000, joestakey, robee
247.8825 USDC - $247.88
Judge has assessed an item in Issue #87 as Medium risk. The relevant finding follows:
Issue https://github.com/code-423n4/2022-04-mimo-findings/issues/3 (Low) - Some tokens do not allow for Non-Zero to Non-Zero value approvals USDT, for example, will not allow approving a non-zero amount and then approving another non-zero amount. The approval must be reset to zero before approving another amount. The following functions will fail on their subsequent calls.
https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/liquidityMining/v2/PARMinerV2.sol#L125 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L97
#0 - gzeoneth
2022-06-05T15:29:47Z
Duplicate of #145
🌟 Selected for report: Dravee
Also found by: 0x1f8b, 0x4non, 0x52, 0xDjango, AlleyCat, Funen, GalloDaSballo, GimelSec, Hawkeye, MaratCerby, Picodes, berndartmueller, cccz, defsec, delfin454000, dipp, hyh, ilan, joestakey, kebabsec, luduvigo, pauliax, peritoflores, robee, rotcivegaf, samruna, shenwilly, sikorico, simon135, sorrynotsorry, unforgiven, z3s
206.9596 USDC - $206.96
There is no reentrancy risk in PARMinerV2.withdraw()
, though it is a recommended practice to follow the CEI pattern regardless. Send the withdrawal amount after decreasing the user's stake.
In the following initialize function, the input address for _owner should be required to not equal address(0).
USDT, for example, will not allow approving a non-zero amount and then approving another non-zero amount. The approval must be reset to zero before approving another amount. The following functions will fail on their subsequent calls.
https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/liquidityMining/v2/PARMinerV2.sol#L125 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L97
A couple initialize functions lack access control. They can be frontrun allowing for crucial variables to be improperly set. This would lead to the contract needing to be redeployed.
https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/inception/InceptionVaultsCore.sol#L40-L58 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L49-L67
stake()
sounds like an action, e.g. for a user to stake the token. Changing to something like getStaked()
would indicate that it's a getter function.
_userInfo.accAmountPerShare = _accMimoAmountPerShare; _userInfo.accParAmountPerShare = _accParAmountPerShare;
Both PAR variables contain "PAR". Only the global MIMO variable contains "MIMO". The user variable should contain "MIMO" in the name as well.
https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L102 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L137 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L187 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L368 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L354
https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L255 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L247
#0 - m19
2022-05-08T05:37:56Z
We appreciate this QA report for clearly listing what could be improved and why. We also appreciate that the author included 1 and 3 in this QA report as opposed to med/high risk which saved everyone a bunch of time.
59.0559 USDC - $59.06
In the following code, a transfer is made from msg.sender to inceptionVaultsCore. Then another transfer is made from inceptionVaultsCore to address(_adminInceptionVault). Might as well just transfer from msg.sender to address(_adminInceptionVault).
The updatedAt
variable is firstly set, then checked if it should be overwritten. Change to a ternary operator and only write the variable once.
updatedAt = assetUpdatedAtA > assetUpdatedAtB ? assetUpdatedAtB : assetUpdatedAtA;