Description:
When the total value of the liquidated asset to be sold to the pool exceeds the total value of 'EUROs' available for the trade, the costInEuros
is erroneously calculated. This results in the execution of the following if block, which wipes out stakers' balances. The miscalculation causes the if block to run, leading to inaccurate changes in stakers' positions.
Impact:
This error wipes out the EUROs position of holders without compensating them with the corresponding reward.
Proof of Concept:
** Given the following scenario: **
Liquidated Asset: wBtc = 1e8 (limiting to one token for simplicity)
btcusd price = 40000e8
wbtc decimals = 8
eurusd price = 1.12e8
_hundredPC = 1e5
_collateralRate = 110000
Position0.TST value = 1000 * 1e0 *1e8 = 1000e8 (amount x price)
Position0.EUROs value = 100 * 1e0 * 1.12e8 = 112e8
Total Staked value = 1112e8
Position1.TST value = 1000 * 1e0 * 1e8 = 1000e18
Position1.EUROs value = 0 * 1e0 * 1.12e8 = 0
Total Staked value = 1000e8
Position2.TST value = 0 * 1e0 * 1e8 = 0
Position2.EUROs value = 100 * 1e0 * 1.12e8 = 112e8
Total Staked value = 112e8
Position3.TST value = 100 * 1e0 * 1e8 = 100e8
Position3.EUROs value = 5 * 1e0 * 1.12e8 = 5.6e8
Total Staked value = 105.6e8
Position4.TST value = 10 * 1e0 * 1e8 = 10e8
Position4.EUROs value = 100 * 1e0 * 1.12e8 = 112e8
Total Staked value = 122e8
Total Value Locked (TVL) = 2441.6e8
Note:
Position0 adds the most value
Position4 adds more value to the pool than Position2, Position1 and Positon3
Position2 adds more value to the pool than Position1 and Positon3
Positon1 adds more value than Position3
Position3 adds the least value
distributeAssets() logic breakdown
stakeTotal = getStakeTotal() == 115
|-- getStakeTotal() = 115
|-- stake() = For Loop:
position0 returns 100
position1 returns 0
position2 returns 0
position3 returns 5
position4 returns 10
Note:
stake() compares _position.TST
and _position.EUROs
amount, not value, which is not a recommended way to compare different tokens.
Position3 return value is greater than Position1 and Position2, despite adding the least value to the protocol.
burnEuros == 0; (tracks EUROs used up by the pool, to be burnt for supply balance)
nativePurchased == 0; (tracks native token bought by the pool, used to calculate tokens returned to the liquidationPoolManager)
For Loop: loop through all holders and distribute rewards to them
{
Loop 1 // for position0
_position = Position0
_positionStake = stake(_position) == 100
(if 100 > 0) {
Loop through each accepted collateral asset (which in our simple example is just wBtc)
asset = wbtc
(if 1e8 > 0) {
_portion = 1e8 * 100 / 115 == 86956521
costInEuros = 86956521 * 1e10 * 40000e8 / 1.12e8 * 1e5 / 110000 == 2.8232637e22
(if 28232.637e18 > 100 ) {
_portion = 86956521 * 100 / 2.8232637e+23 == 3.08e-14 == 0
costInEuros = 100
}
Position0.EUROs -= 100 // wipes out stakers position
burnEuros += 100
(if reward native is token) {
// We are not dealing with a native token
}
(if reward native is erc20) {
// transfers 0 tokens to the pool.
}
}
}
// Update Position0
}
// After looping through all holders it:
// burns the burnEuros
// Return native tokens that weren't bought
Note:
I changed 10e10 to 1e10 in costInEuros calculation to fix a bug that causes the value to incorrectly increase by an order of magnitude.
Position0 EUROs position is drained but receives zero reward
Position0 EUROs are lost permanently as it is added to burnEuros
Position0 updated position:
Position0.TST value = 1000 * 1e8 (amount x price)
Position0.EUROs value = 0 * 20e8
Total Staked value = 1000e8
Net Loss is 2000
Key:
{logic}: Logic block
bold font : code variables
(conditional statements)
Proof of Code:
The provided test suite demonstrates the vulnerability's validity and severity.
Due to the file size required to run this PoC, the suite is hosted on Github.
To run the PoC, clone the repository.
Minor changes, such as modifying function visibility, were made to enable successful test runs.
All changes and additional files made to the original code are documented in the README and the respective files where the changes are made.
Requirements:
Install Foundry.
Clone the project codebase into your local workspace.
Run the following commands to install dependencies:
Run the following command to execute the PoC:
Tools Used:
Manual review
Foundry
Recommended Mitigation Steps:
Stake()
should return the USD value of stakers' positions.
Add a condition to check if the Euro value of the total asset to be sold is greater than the Euros available to purchase them (Euro position of stakers eligible for reward). If true, cap the value of the amount of assets to sell to the total Euro available for trade.
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.