The distributeAssets() function has no access control and can be manipulated by any confirmed staker to increase their ETH reward to any value they wish to. There is no cost attached to the attack apart from the gas cost.
Add a file test\pwned.js with the following code and run via npx hardhat test --grep 'is pwned' to see it pass.
The 3 params of the function distributeAssets() can be created manually:
The PoC code creates the ILiquidationPoolManager.Asset[] memory _assets parameter by using ETH token symbol 0x4554480000000000000000000000000000000000000000000000000000000000, its address as 0 and its chainlink feed address as 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 all of which are real, valid values. However, inside the _assets struct it sets the amount as an arbitrary value which the attacker wants to siphon off (52 ETH in our case).
Also, we will pass _hundredPC as zero. This is because the function calculates costInEuros, which is the cost the holder needs to pay. By setting _hundredPC as zero, it evaluates to 0 and no cost will have to be paid.
Now, all the attacker has to do is wait for the LiquidationPool contract to have the desired amount of ETH in it and then call claimRewards to get the ETH credited into his wallet. Notice that this transfer of ETH into the LiquidationPool contract from the LiquidationPoolManager contract happens automatically when a vault is liquidated by anybody calling the runLiquidation() function. Once that call completes, the attacker can call claimRewards() and effectively steal someone else's share of rewards. The other holder will be left without any ETH to claim even though his rewards variable will show that he is eligible.
In fact, the best attack path for the attacker would be -
Position himself as a confirmed holder by staking the minimum amount so that he can attack later.
Monitor the mempool for someone to call runLiquidation(). Let's call this transaction tx.
Calculate & note the ETH rewards which will be distributed in this call.
Front-run tx and directly call distributeAssets() with his maliciously crafted parameters, keeping amount as the total ETH reward in tx.
Back-run tx and call claimRewards() to get away with stealing the entire ETH before anyone is the wiser. Then call decreasePosition() to remove his miniscule staked amount too.
Other holders calling claimRewards() now will see their call revert as there are no funds inside LiquidationPool.
The PoC shows only one holder in the pool. If there are more holders, this attack increments their rewards variable too. However the attacker will move first to claim the rewards hence leaving no ETH for others.
This would work for any other ERC20 collateral token too, if the approve() is in place - that is, the LiquidationPoolManager has approved the amount before the attack.
Major loss of protocol funds
Some other holder loses his reward
Hardhat
Add the onlyManager access modifier to the distributeAssets() function:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.