The function LiquidationPool::distributeAssets()
is an external function that has no access control modifier, and hence it can be called by anyone. This method is meant to be called by LiquidationPoolManager::runLiquidation()
which liquidates a vault, and then uses the LiquidationPool::distributeAssets()
function to distribute the rewards from the liquidation amongst the holders that maintain a position.
The stakes position of users is measured by min of EUROs and TST token. Upon distribution of the rewards, the EUROs position of the holder is reduced equal to the rewards being distributed in EUROs.
LiquidationPool::distributeAssets()
takes in collteral rate as an input, which can be used by the attacker to inflate the amount by which the EUROs is reduced for the holders to 0, ultimately making their overall position as 0. It also takes assets as an input, which can be an arbitrary token that is not recognized by the protocol, further making the holders lose all the rewards.
Below is the method signature of LiquidationPool::distributeAssets()
for reference.
Below is a summary of how the LiquidationPool::distributeAssets()
function works:
Function gets called with assets, and collateral rate, and a value for "hundred percent with the right precision"
Next, it gets the total stake amount of across all the holders.
For each of the holder, it does this:
Get stake of the current holder, which is the minimum of TST or EUROs position of the holder
For all the assets, do this:
Calculate the stake portion of the holder ((holder stake/total stake) * 100)
[ATTACK_REF_1] Calculate the stake position for the given asset for the holder in terms of EUROs. It takes the collateral rate into account. The key point here is that the collateral rate is taken as the input, and collateral rate is used to divide the overall value. Its expected that the collateral rate would be something like 120000 (120%), but instead the attacker can just pass a value of 1, which would inflate the EUROs amount for the user.
[ATTACK_REF_2] Reduce the EUROs position of the holder by the above calculated amount. If the holder doesn't have enough EUROs, then their portion is updated to what their whole position is, thereby making the position after distribution to 0
The rest of the loop deals with token transfers from the Liquidation Pool Manager and burning of EUROs
Now, because LiquidationPool::distributeAssets()
has no access control modifier and is an external function, therefore anyone can call this function. The attacker can pass any value for the _collateralRate
. If the attacker calls the function with a large enough value for _collateralRate
, such that the holders position in terms of EUROs (as mentioned in ATTACK_REF_1) exceeds their EUROs position, then complete position is used to distribute the assets and their position is updated to 0 (ATTACK_REF_2).
Drop the below test in test/liquidationPool.js
(The test is using describe.only
, so when you run the test, then only this test will be run)
An attacker can call LiquidationPool::distributeAssets()
with a value of 1 collateral rate right before LiquidationPoolManager::runLiquidation()
is called, and make everyone's position as 0. This will lead to loss of EUROs for all the holders, and the attacker can furthermore then increase their positions for the next liquidations to get major chunk of the liquidation amount.
The attack can be further exacerbated if the attacker uses an asset that is not recognized by the contracts. Because the asset is not part of the "Accepted Tokens", the holders get no rewards at all.
In the worst case scenario, the attacker can accomplish this:
Frontrun the call to LiquidationPoolManager::runLiquidation()
Execute the "proof of concept". This will eliminate all the holders, and make their positions as 0.
The attacker can increase their position in the Liquidation Pool.
LiquidationPoolManager::runLiquidation()
gets called
The attacker gets all the complete share of the liquidation of the vault
Alternatively, the attacker can also just grief all the holders by front running LiquidationPoolManager::runLiquidation()
with the "proof of concept", and the holders will end up losing their position (EUROs) and get no rewards either.
Please note that frontrunning is just a mechanism to showcase the attack. Frontrunning is not a requirement.
Manual Review
Add access control modifier to LiquidationPool::distributeAssets()
so that only LiquidationPoolManager
can call it.
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.