When the basket isn't fully collateralized, a recollateralization process takes place, which is crucial.
The basket consists of several tokens, and during this process, surplus tokens are sold to buy those that are in deficit.
To determine the amounts to sell and buy, we first set a range of baskets that can be achieved after optimal trading, referred to as [bottom, top].
If a token has enough to cover the top of this range, the excess can be sold, but it should not fall below the top.
Conversely, if a token does not have enough to cover the bottom, we need to buy more, with the amount based on the bottom.
However, there's an issue: the bottom calculation is incorrect.
It's essential to fully collateralize the baskets as quickly as possible.
Even if we could achieve full collateralization in one trade, the incorrect bottom calculation might prevent this, requiring manual adjustment of the basketsNeeded, which could lead to losses for depositors.
Alternatively, the trading could be split into several smaller trades, each taking time—potentially up to a week per trade.
Proof of Concept
Below is the basketRange function, which determines the target basket range.
In line 148, if the quantity of a token is 0, it means this token is not a backing token for the basket, so we can sell all of these tokens. However, the sale must meet the minimum trading volume. If a token's quantity is insufficient to satisfy this minimum, we skip it.
By the time we reach line 198, the token is either a backing token or a non-backing token with a quantity sufficient to exceed the minimum trading volume. At this point, all backing tokens should have enough quantity to cover the current bottom.
The idea is to sell all excess tokens at a lower price, buy baskets at a higher price, and see the final bottom we can achieve.
It seems the reason for subtracting the minimum trading volume in line 198 is to ensure we trade at least that volume.
However, if the token is not a backing token and its quantity is greater than the minimum trading volume, we could sell all of it.
If the token is a backing token with less than the minimum trading volume, we didn’t actually sell these tokens, but they should still be included in the target bottom calculation.
For example, consider two backing tokens, A and B, and one non-backing token, C. All three tokens are priced at 1, with A having a quantity of 2, B having 1, and C having 2.
The minimum trading volume is 2, and the current bottom is 1.
We could sell 2 C to buy 2 B, increasing the bottom to 2.
However, in line 198, the target bottom is still calculated as 1, so no trade is executed.
Therefore, there’s no need to subtract the minimum trading volume in line 198.
Let me clarify with a specific example:
Consider two tokens: token0 and token1, where token1 is a backing token and token0 is not. Their prices and the basket's price are as follows.
low price of token 0 => BigNumber { value: "990000000000000000" }
high price of token 0 => BigNumber { value: "1010000000000000000" }
low price of token 1 => BigNumber { value: "990000000000000000" }
high price of token 1 => BigNumber { value: "1010000000000000000" }
low price of basket => BigNumber { value: "990000000000000000" }
high price of basket => BigNumber { value: "1010000000000000000" }
Currently, there are no token1 in the backing manager, and the current basket bottom and basketsNeeded are:
To fully collateralize, we need to purchase 100000000000000000000 token1, which requires selling 102020202020202020203 token0 (we're selling token0 at a lower price and buying token1 at a higher price).
The minimum trading volume is currently 20e18, and this amount is subtracted from the target bottom calculation in line 198.
As a result, even though we have enough token0 to buy the required token1 and fully collateralize, the determined sell and buy amounts are insufficient, so the basket remains undercollateralized.
After this trade, there won’t be enough funds left to execute another trade because the remaining token0 amount is just equal to the minimum trading volume, which won't allow the target bottom to increase.
Consequently, the compromiseBasketsNeeded function will be called to manually decrease the basketsNeeded, leading to a loss for depositors
Please add below test to the test/Recollateralization.test.ts.
Lines of code
https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/mixins/RecollateralizationLib.sol#L198
Vulnerability details
Impact
When the
basket
isn't fullycollateralized
, arecollateralization
process takes place, which is crucial. Thebasket
consists of several tokens, and during this process, surplus tokens are sold to buy those that are in deficit. To determine the amounts to sell and buy, we first set a range ofbaskets
that can be achieved after optimal trading, referred to as[bottom, top]
. If a token has enough to cover thetop
of this range, the excess can be sold, but it should not fall below thetop
. Conversely, if a token does not have enough to cover thebottom
, we need to buy more, with the amount based on thebottom
. However, there's an issue: thebottom
calculation is incorrect. It's essential to fullycollateralize
thebaskets
as quickly as possible. Even if we could achieve fullcollateralization
in one trade, the incorrectbottom
calculation might prevent this, requiring manual adjustment of thebasketsNeeded
, which could lead to losses for depositors. Alternatively, the trading could be split into several smaller trades, each taking time—potentially up to a week per trade.Proof of Concept
Below is the
basketRange
function, which determines the targetbasket
range.In
line 148
, if thequantity
of a token is0
, it means this token is not abacking token
for thebasket
, so we can sell all of these tokens. However, the sale must meet theminimum trading volume
. If a token's quantity is insufficient to satisfy this minimum, we skip it.By the time we reach
line 198
, the token is either abacking token
or anon-backing token
with a quantity sufficient to exceed theminimum trading volume
. At this point, allbacking tokens
should have enough quantity to cover the currentbottom
. The idea is to sell all excess tokens at alower price
, buybaskets
at ahigher price
, and see the finalbottom
we can achieve. It seems the reason for subtracting theminimum trading volume
inline 198
is to ensure we trade at least that volume.However, if the token is not a
backing token
and its quantity is greater than theminimum trading volume
, we could sell all of it. If the token is abacking token
with less than theminimum trading volume
, we didn’t actually sell these tokens, but they should still be included in the targetbottom
calculation.Therefore, there’s no need to subtract the
minimum trading volume
inline 198
.Let me clarify with a specific example:
Consider two tokens:
token0
andtoken1
, wheretoken1
is a backing token andtoken0
is not. Their prices and the basket's price are as follows.Currently, there are no
token1
in thebacking manager
, and the currentbasket bottom
andbasketsNeeded
are:To fully
collateralize
, we need to purchase100000000000000000000 token1
, which requires selling102020202020202020203 token0
(we're sellingtoken0
at a lower price and buyingtoken1
at a higher price).The
minimum trading volume
is currently20e18
, and this amount is subtracted from thetarget bottom
calculation inline 198
.As a result, even though we have enough
token0
to buy the requiredtoken1
and fullycollateralize
, the determined sell and buy amounts are insufficient, so the basket remains undercollateralized.After this trade, there won’t be enough funds left to execute another trade because the remaining
token0
amount is just equal to theminimum trading volume
, which won't allow thetarget bottom
to increase. Consequently, thecompromiseBasketsNeeded
function will be called to manually decrease thebasketsNeeded
, leading to a loss for depositorsPlease add below test to the
test/Recollateralization.test.ts
.Tools Used
Recommended Mitigation Steps
Assessed type
Math