-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix arithmetic underflow in the StabilityPool #438
Changes from 23 commits
cc314ca
b54822b
29c9d80
7c09b6a
ee32ae8
d6c6bf6
6802c59
e54ba8d
2f5a627
bd46494
fdd6c6c
1c05912
aeffca6
120792d
991a80b
9ce60db
7f862c6
d147474
6783051
1134158
389aeb6
0a297bd
e6b56c0
4e4b95f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -198,6 +198,7 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
// Error trackers for the error correction in the offset calculation | ||
uint256 public lastCollError_Offset; | ||
uint256 public lastBoldLossError_Offset; | ||
uint256 public lastBoldLossError_TotalDeposits; | ||
|
||
// Error tracker fror the error correction in the BOLD reward calculation | ||
uint256 public lastYieldError; | ||
|
@@ -408,7 +409,7 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
uint256 yieldPerUnitStaked = yieldNumerator / totalBoldDepositsCached; | ||
lastYieldError = yieldNumerator - yieldPerUnitStaked * totalBoldDepositsCached; | ||
|
||
uint256 marginalYieldGain = yieldPerUnitStaked * P; | ||
uint256 marginalYieldGain = yieldPerUnitStaked * (P - 1); | ||
epochToScaleToB[currentEpoch][currentScale] = epochToScaleToB[currentEpoch][currentScale] + marginalYieldGain; | ||
|
||
emit B_Updated(epochToScaleToB[currentEpoch][currentScale], currentEpoch, currentScale); | ||
|
@@ -426,10 +427,7 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
uint256 totalBold = totalBoldDeposits; // cached to save an SLOAD | ||
if (totalBold == 0 || _debtToOffset == 0) return; | ||
|
||
(uint256 collGainPerUnitStaked, uint256 boldLossPerUnitStaked) = | ||
_computeCollRewardsPerUnitStaked(_collToAdd, _debtToOffset, totalBold); | ||
|
||
_updateCollRewardSumAndProduct(collGainPerUnitStaked, boldLossPerUnitStaked); // updates S and P | ||
_updateCollRewardSumAndProduct(_collToAdd, _debtToOffset, totalBold); // updates S and P | ||
|
||
_moveOffsetCollAndDebt(_collToAdd, _debtToOffset); | ||
} | ||
|
@@ -438,7 +436,7 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
|
||
function _computeCollRewardsPerUnitStaked(uint256 _collToAdd, uint256 _debtToOffset, uint256 _totalBoldDeposits) | ||
internal | ||
returns (uint256 collGainPerUnitStaked, uint256 boldLossPerUnitStaked) | ||
returns (uint256 collGainPerUnitStaked, uint256 boldLossPerUnitStaked, uint256 newLastBoldLossErrorOffset) | ||
{ | ||
/* | ||
* Compute the Bold and Coll rewards. Uses a "feedback" error correction, to keep | ||
|
@@ -457,33 +455,39 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
if (_debtToOffset == _totalBoldDeposits) { | ||
boldLossPerUnitStaked = DECIMAL_PRECISION; // When the Pool depletes to 0, so does each deposit | ||
lastBoldLossError_Offset = 0; | ||
lastBoldLossError_TotalDeposits = _totalBoldDeposits; | ||
} else { | ||
uint256 boldLossNumerator = _debtToOffset * DECIMAL_PRECISION - lastBoldLossError_Offset; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering was this right in the first place (or maybe I need to refresh). Prior to this PR we were subtracting the error from the bold loss (L461). But shouldn't we add it, as we do for the coll? Let's consider what we were trying to fix, if there was no error correction at all:
So with no error correction it's like that. But by subtracting the last BOLD error, don't we make it worse: In contrast, with the coll (line 452), we add the last coll error, which makes sense: coll rewards are too small from floor div, so we're storing the error and adding it. I wonder if simply changing the sign here in L461 would fix underflow (or if its not so simple)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t think that’s what we were trying to fix with the error. That we do it with the
The problem is that the second step can revert the effect of the first one to some extent, and make the underflows appear again (if I’m not mistaken because the treatment of that error was wrong). That’s what we are trying to fix in this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, coll rewards have the opposite direction because it’s a floor division (as opposed to the ceil division of bold deposits). I.e., we don’t do the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ahhh right. Thanks, yes that makes it clear! |
||
uint256 boldLossNumerator = _debtToOffset * DECIMAL_PRECISION; | ||
/* | ||
* Add 1 to make error in quotient positive. We want "slightly too much" Bold loss, | ||
* which ensures the error in any given compoundedBoldDeposit favors the Stability Pool. | ||
*/ | ||
boldLossPerUnitStaked = boldLossNumerator / _totalBoldDeposits + 1; | ||
lastBoldLossError_Offset = boldLossPerUnitStaked * _totalBoldDeposits - boldLossNumerator; | ||
newLastBoldLossErrorOffset = boldLossPerUnitStaked * _totalBoldDeposits - boldLossNumerator; | ||
} | ||
|
||
collGainPerUnitStaked = collNumerator / _totalBoldDeposits; | ||
lastCollError_Offset = collNumerator - collGainPerUnitStaked * _totalBoldDeposits; | ||
|
||
return (collGainPerUnitStaked, boldLossPerUnitStaked); | ||
return (collGainPerUnitStaked, boldLossPerUnitStaked, newLastBoldLossErrorOffset); | ||
} | ||
|
||
// Update the Stability Pool reward sum S and product P | ||
function _updateCollRewardSumAndProduct(uint256 _collGainPerUnitStaked, uint256 _boldLossPerUnitStaked) internal { | ||
function _updateCollRewardSumAndProduct(uint256 _collToAdd, uint256 _debtToOffset, uint256 _totalBoldDeposits) | ||
internal | ||
{ | ||
(uint256 collGainPerUnitStaked, uint256 boldLossPerUnitStaked, uint256 newLastBoldLossErrorOffset) = | ||
_computeCollRewardsPerUnitStaked(_collToAdd, _debtToOffset, _totalBoldDeposits); | ||
|
||
uint256 currentP = P; | ||
uint256 newP; | ||
|
||
assert(_boldLossPerUnitStaked <= DECIMAL_PRECISION); | ||
assert(boldLossPerUnitStaked <= DECIMAL_PRECISION); | ||
/* | ||
* The newProductFactor is the factor by which to change all deposits, due to the depletion of Stability Pool Bold in the liquidation. | ||
* We make the product factor 0 if there was a pool-emptying. Otherwise, it is (1 - boldLossPerUnitStaked) | ||
*/ | ||
uint256 newProductFactor = uint256(DECIMAL_PRECISION) - _boldLossPerUnitStaked; | ||
uint256 newProductFactor = uint256(DECIMAL_PRECISION) - boldLossPerUnitStaked; | ||
|
||
uint128 currentScaleCached = currentScale; | ||
uint128 currentEpochCached = currentEpoch; | ||
|
@@ -496,7 +500,7 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
* | ||
* Since S corresponds to Coll gain, and P to deposit loss, we update S first. | ||
*/ | ||
uint256 marginalCollGain = _collGainPerUnitStaked * currentP; | ||
uint256 marginalCollGain = collGainPerUnitStaked * (currentP - 1); | ||
uint256 newS = currentS + marginalCollGain; | ||
epochToScaleToS[currentEpochCached][currentScaleCached] = newS; | ||
emit S_Updated(newS, currentEpochCached, currentScaleCached); | ||
|
@@ -524,8 +528,14 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
emit ScaleUpdated(currentScale); | ||
// If there's no scale change and no pool-emptying, just do a standard multiplication | ||
} else { | ||
newP = currentP * newProductFactor / DECIMAL_PRECISION; | ||
uint256 errorFactor; | ||
if (lastBoldLossError_Offset > 0) { | ||
errorFactor = lastBoldLossError_Offset * newProductFactor / lastBoldLossError_TotalDeposits; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trying to understand the rationale for the denominator here.
So that means 1) if the current liq is pool-emptying, we use current Is that intended? If so, why? In case 1), we use the most recent totalBoldDeposits, which could have changed since the last liq as deposits were added/removed. In 2, we use a potentially outdated stored value. I'd have assumed we should always use the current value, since we're scaling a past error (calculated using past total deposits) by current total deposits. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, that’s intended. In case 1 we are kind of resetting everything, Anyway, there’s a TODO in this PR to review the case for scale updates, there may be something missing (although as far I know, it hasn’t surfaced during fuzzing yet). So, it’s intended, but I’m not 100% sure it’s correct. I want to get deep into that case once we make sure the basic logic (for the generic case where |
||
} | ||
newP = (currentP * newProductFactor + errorFactor) / DECIMAL_PRECISION; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems consistent with the previous implementation, i.e. making There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Answered above. |
||
} | ||
lastBoldLossError_Offset = newLastBoldLossErrorOffset; | ||
lastBoldLossError_TotalDeposits = _totalBoldDeposits; | ||
|
||
assert(newP > 0); | ||
P = newP; | ||
|
@@ -577,8 +587,22 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
|
||
Snapshots memory snapshots = depositSnapshots[_depositor]; | ||
|
||
uint256 collGain = _getCollGainFromSnapshots(initialDeposit, snapshots); | ||
return collGain; | ||
/* | ||
* Grab the sum 'S' from the epoch at which the stake was made. The Coll gain may span up to one scale change. | ||
* If it does, the second portion of the Coll gain is scaled by 1e9. | ||
* If the gain spans no scale change, the second portion will be 0. | ||
*/ | ||
uint128 epochSnapshot = snapshots.epoch; | ||
uint128 scaleSnapshot = snapshots.scale; | ||
uint256 S_Snapshot = snapshots.S; | ||
uint256 P_Snapshot = snapshots.P; | ||
|
||
uint256 firstPortion = epochToScaleToS[epochSnapshot][scaleSnapshot] - S_Snapshot; | ||
uint256 secondPortion = epochToScaleToS[epochSnapshot][scaleSnapshot + 1] / SCALE_FACTOR; | ||
|
||
uint256 collGain = initialDeposit * (firstPortion + secondPortion) / P_Snapshot / DECIMAL_PRECISION; | ||
|
||
return LiquityMath._min(collGain, collBalance); | ||
} | ||
|
||
function getDepositorYieldGain(address _depositor) public view override returns (uint256) { | ||
|
@@ -588,8 +612,22 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
|
||
Snapshots memory snapshots = depositSnapshots[_depositor]; | ||
|
||
uint256 yieldGain = _getYieldGainFromSnapshots(initialDeposit, snapshots); | ||
return yieldGain; | ||
/* | ||
* Grab the sum 'B' from the epoch at which the stake was made. The Bold gain may span up to one scale change. | ||
* If it does, the second portion of the Bold gain is scaled by 1e9. | ||
* If the gain spans no scale change, the second portion will be 0. | ||
*/ | ||
uint128 epochSnapshot = snapshots.epoch; | ||
uint128 scaleSnapshot = snapshots.scale; | ||
uint256 B_Snapshot = snapshots.B; | ||
uint256 P_Snapshot = snapshots.P; | ||
|
||
uint256 firstPortion = epochToScaleToB[epochSnapshot][scaleSnapshot] - B_Snapshot; | ||
uint256 secondPortion = epochToScaleToB[epochSnapshot][scaleSnapshot + 1] / SCALE_FACTOR; | ||
|
||
uint256 yieldGain = initialDeposit * (firstPortion + secondPortion) / P_Snapshot / DECIMAL_PRECISION; | ||
|
||
return LiquityMath._min(yieldGain, yieldGainsOwed); | ||
} | ||
|
||
function getDepositorYieldGainWithPending(address _depositor) external view override returns (uint256) { | ||
|
@@ -600,13 +638,14 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
Snapshots memory snapshots = depositSnapshots[_depositor]; | ||
|
||
uint256 pendingSPYield = activePool.calcPendingSPYield() + yieldGainsPending; | ||
uint256 newYieldGainsOwed = yieldGainsOwed + (totalBoldDeposits >= DECIMAL_PRECISION ? pendingSPYield : 0); | ||
uint256 firstPortionPending; | ||
uint256 secondPortionPending; | ||
|
||
if (pendingSPYield > 0 && snapshots.epoch == currentEpoch && totalBoldDeposits >= DECIMAL_PRECISION) { | ||
uint256 yieldNumerator = pendingSPYield * DECIMAL_PRECISION + lastYieldError; | ||
uint256 yieldPerUnitStaked = yieldNumerator / totalBoldDeposits; | ||
uint256 marginalYieldGain = yieldPerUnitStaked * P; | ||
uint256 marginalYieldGain = yieldPerUnitStaked * (P - 1); | ||
|
||
if (currentScale == snapshots.scale) firstPortionPending = marginalYieldGain; | ||
else if (currentScale == snapshots.scale + 1) secondPortionPending = marginalYieldGain; | ||
|
@@ -616,53 +655,9 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
uint256 secondPortion = | ||
(epochToScaleToB[snapshots.epoch][snapshots.scale + 1] + secondPortionPending) / SCALE_FACTOR; | ||
|
||
return initialDeposit * (firstPortion + secondPortion) / snapshots.P / DECIMAL_PRECISION; | ||
} | ||
uint256 yieldGain = initialDeposit * (firstPortion + secondPortion) / snapshots.P / DECIMAL_PRECISION; | ||
|
||
function _getCollGainFromSnapshots(uint256 initialDeposit, Snapshots memory snapshots) | ||
internal | ||
view | ||
returns (uint256) | ||
{ | ||
/* | ||
* Grab the sum 'S' from the epoch at which the stake was made. The Coll gain may span up to one scale change. | ||
* If it does, the second portion of the Coll gain is scaled by 1e9. | ||
* If the gain spans no scale change, the second portion will be 0. | ||
*/ | ||
uint128 epochSnapshot = snapshots.epoch; | ||
uint128 scaleSnapshot = snapshots.scale; | ||
uint256 S_Snapshot = snapshots.S; | ||
uint256 P_Snapshot = snapshots.P; | ||
|
||
uint256 firstPortion = epochToScaleToS[epochSnapshot][scaleSnapshot] - S_Snapshot; | ||
uint256 secondPortion = epochToScaleToS[epochSnapshot][scaleSnapshot + 1] / SCALE_FACTOR; | ||
|
||
uint256 collGain = initialDeposit * (firstPortion + secondPortion) / P_Snapshot / DECIMAL_PRECISION; | ||
|
||
return collGain; | ||
} | ||
|
||
function _getYieldGainFromSnapshots(uint256 initialDeposit, Snapshots memory snapshots) | ||
internal | ||
view | ||
returns (uint256) | ||
{ | ||
/* | ||
* Grab the sum 'B' from the epoch at which the stake was made. The Bold gain may span up to one scale change. | ||
* If it does, the second portion of the Bold gain is scaled by 1e9. | ||
* If the gain spans no scale change, the second portion will be 0. | ||
*/ | ||
uint128 epochSnapshot = snapshots.epoch; | ||
uint128 scaleSnapshot = snapshots.scale; | ||
uint256 B_Snapshot = snapshots.B; | ||
uint256 P_Snapshot = snapshots.P; | ||
|
||
uint256 firstPortion = epochToScaleToB[epochSnapshot][scaleSnapshot] - B_Snapshot; | ||
uint256 secondPortion = epochToScaleToB[epochSnapshot][scaleSnapshot + 1] / SCALE_FACTOR; | ||
|
||
uint256 yieldGain = initialDeposit * (firstPortion + secondPortion) / P_Snapshot / DECIMAL_PRECISION; | ||
|
||
return yieldGain; | ||
return LiquityMath._min(yieldGain, newYieldGainsOwed); | ||
} | ||
|
||
// --- Compounded deposit --- | ||
|
@@ -697,14 +692,18 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { | |
uint256 compoundedStake; | ||
uint128 scaleDiff = currentScale - scaleSnapshot; | ||
|
||
// To make sure rouning errors favour the system, we use P - 1 if P decreased | ||
uint256 cachedP = P; | ||
uint256 currentPToUse = cachedP != snapshot_P ? cachedP - 1 : cachedP; | ||
|
||
/* Compute the compounded stake. If a scale change in P was made during the stake's lifetime, | ||
* account for it. If more than one scale change was made, then the stake has decreased by a factor of | ||
* at least 1e-9 -- so return 0. | ||
*/ | ||
if (scaleDiff == 0) { | ||
compoundedStake = initialStake * P / snapshot_P; | ||
compoundedStake = initialStake * currentPToUse / snapshot_P; | ||
} else if (scaleDiff == 1) { | ||
compoundedStake = initialStake * P / snapshot_P / SCALE_FACTOR; | ||
compoundedStake = initialStake * currentPToUse / snapshot_P / SCALE_FACTOR; | ||
} else { | ||
// if scaleDiff >= 2 | ||
compoundedStake = 0; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully get the use of
P - 1
rather than P. As far as I can see:Why do we then use P -1? Doesn't that make it even smaller than it should be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rationale is that we end up using
x * P_n / P_m
, withn > m
(and beingP_n
the one in the line above.The fraction
P_n / P_m
represents the ratio of total deposits between those two snapshots (m
andn
).We want the ratio between
P
’s to always be <= than the ratio between deposits. So that rewards shared are less or equal than should be, but not greater.As both
P_n
andP_m
are inaccurate, it may happen than the error (truncating) inP_m
is bigger than inP_n
. That would make the fraction to increase.Let’s put an example, assuming
DECIMAL_PRECISION = 100
. Let’s imagine that the total deposits are 4999 and 2001 form
andn
snapshots, and that theoretical values ofP
would be 49.99 (truncated to 49) and 20.01 (truncated to 20). Then we haveP_n / P_m = 20 / 49 > 2001 / 4999 = deposit_n / deposit_m
. That’s what we wan to avoid, so we useP_n = 19
instead of 20. Of course in this example this makes the precision ofP_n / P_m
very bad, but this is an edge case and mostly due to the low precision (only 2 decimals). With 18 decimals, subtracting 1 wouldn’t be such big deal.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, yes I see. So this correction should always make the error in
P_n
bigger than the error inP_m
. Cool, it makes sense.