The current implementation of the fix of H-01 uses ulow
and ucrit
as the lower and upper bounds for when the AMO is active.
This seemed right on our call, but after digging into the math, it doesn't actually achieve the desired effect. This is because, there are times when manipulating the Silo utilization rate within those bounds can lead to a response that moves it far outside these bounds.
This means the fix is protective against the H-04 issue (shutting down when we face extreme unpredictable swings) but not against the intentional manipulation in H-01 and H-02.
Let's look at the two examples where things can get outside the range.
First, let's imagine the flow where a user deposits to bait the AMO into withdrawing:
- There are currently 5 OHM borrowed and 10 deposits, for perfect 0.5
uopt
- The user deposits to lower the util as much as possible while staying within
ulow
toucrit
(deposits 6.66 makes util 5/16.66 = 0.3) - The AMO responds by withdrawing 6.66 to regain equilibrium (5/10)
- The user withdraws 5 out of their 6.66 deposit, bringing util to 100% (5/5)
Second, we can look at the situation where a user borrows to bait the AMO into depositing:
- There are currently 5 OHM borrowed and 10 deposits, for perfect 0.5
uopt
- A user borrows 4 OHM to bring util up to 0.9 (9/10)
- The AMO responds by depositing 8 OHM to bring it back to 0.5 (9/18)
- THe user repays their 4 OHM, pushing it outside the window to 0.27 (5/18)
Clearly these are not acceptable!
Looking at the different ways in which a pool could be manipulated:
- DEPOSIT (above): can force util up to 100%
- WITHDRAW: can force util down to 34% (fine!)
- BORROW (above): can force util down to 27%
- REPAY: can force util up to 83% (fine!)
What we need to do is to calculate the utilization rate that, when manipulated into that state to prompt an AMO response, results in a swing back that keeps us within the ulow
to ucrit
window.
This actually isn't too difficult to calculate, so we can do it on chain to get better bound values.
For the lower bound, we can calculate:
borrows / ((2 / uopt * borrows) - (borrows / ucrit))
We can derive this formula by noticing that:
- when at
uopt
,deposits = borrows / uopt
- when at
ucrit
,deposits = borrows / ucrit
Therefore, the swing in deposits needed to move us from uopt
to ucrit
is equal to (borrows / uopt) - (borrows / ucrit)
.
In order to prevent a manipulation from pushing the AMO to cause a swing of that size, we need to ensure that a swing of that size isn't applied by a user in the other direction. This is because, if a user deposits that amount, the AMO will withdraw that amount, and then the user can withdraw their deposit to have that magnitude of impact.
Since the most we can allow the deposits to swing is (borrows / uopt) - (borrows / ucrit)
, we can calculate the lowerBound by seeing where the utilization ends if a user deposits that quantity to a Silo that is current balanced at uopt
:
lowerBound = borrows / (borrows / uopt + ((borrows / uopt) - (borrows / ucrit)))
lowerBound = borrows / ((2 * borrows / uopt) - (borrows / ucrit))
lowerBound = 1 / ((2 / uopt) - (1 / ucrit))
Looking at an example, if we have a Silo with 5 OHM of borrows and 10 OHM of deposits:
- acceptable swing =
(borrows / uopt) - (borrows / ucrit)
=5/0.5 - 5/0.9
=4.4444
- maximum acceptable deposits =
10 + 4.4444
=14.4444
- minimum acceptable util =
5 / 14.4444
=0.346
- we can also calculate this as
1 / ((2 / uopt) - (1 / ucrit))
=1 / ((2 / 0.5) - (1 / 0.9)
=1 / (4 - 1.111)
=0.346
For the upper bound, we can perform similar calculations:
- First, we get the maximum deposits value we're comfortable with, which is
borrows / ulow
- Then, we get the deviation in deposits that would lead to this value, which is
(borrows / ulow) - (borrows / uopt)
- Then, we calculate how much would need to be borrowed to get the AMO to deposit this much:
((borrows / ulow) - (borrows / uopt)) * uopt
- Finally, we get the util ratio when that much is borrowed:
borrows + ((borrows / ulow) - (borrows / uopt)) * uopt / (borrows / uopt)
- Simplified (thanks to Liened), this can be represented by:
uopt + (uopt * ((uopt / ulow) - 1))
Looking at an example with 5 OHM of borrows and 10 OHM of deposits:
- max acceptable borrows =
borrows / ulow
=5 / 0.3
=16.66
- max acceptable swing =
(borrows / ulow) - (borrows / uopt)
=16.66 - 10
=6.66
- max acceptable borrows =
((borrows / ulow) - (borrows / uopt)) * uopt
=6.66 * 0.5
=3.33
- max acceptable util =
borrows + ((borrows / ulow) - (borrows / uopt)) * uopt / (borrows / uopt)
=(5 + 3.33) / (5 / 0.5)
=0.833
- simplified calculation =
uopt + (uopt * ((uopt / ulow) - 1))
=0.5 + (0.5 * ((0.5 / 0.3) - 1))
=0.5 + 0.333
=0.833
When checking the utilization rate, confirm that it falls within:
- min:
1 / ((2 / uopt) - (1 / ucrit))
- max:
uopt + (uopt * ((uopt / ulow) - 1))