Closed sherlock-admin2 closed 1 month ago
There is a bug in the python PoC. The diff calculation doesn't correctly prioritize the arithmetic operations (which are not commutative due to the use of //). That line should be: mint += (totalSupply * (block_mul * chi // RAY) // RAY) - (totalSupply * chi // RAY)
(note the addition of the inner parentheses). With that line fixed, you can verify that there is no shortfall.
mrhudson890
Medium
SNst.drip()
's incremental NST minting causes compounding asset shortfall and withdrawer lossesSummary
Precision loss in the SNst contract's drip mechanism during NST minting causes a cumulative asset shortfall for SNst holders. This results in the last withdrawer receiving fewer assets than expected and being unable to redeem all their shares. The amount is calculated in a PoC to be significant for users (above 0.5% for final value of 10K). Additionally, key ERC-4626 methods will revert, and view functions return incorrect values.
Root Cause
Using calculated
chi
as ratio of assets / shares (as was done inpot
andsDai
) requires RAD (1e45) credit tracking precision. However, available assets for withdrawals are the actual ERC-20 balances of minted NST, tracked in WAD precision (1e18). This causes the ratio of available assets to shares to be significantly different from the needed assets due to accumulated compounding precision losses.First, it's useful to mention the precise method used in the current
sDai
, which uses the Pot contract's drip. In it, WAD ERC-20 assets are minted ONLY when they are exited during withdrawal (burn) - when they cease to grow withchi
. This ensures exact correspondence betweenchi * shares
(RAD precision) andvat.dai
internal credit's, and the eventually minted assets. This allows the usage of chi through the contract as a share price.However,
SNst.sol:drip()
:totalSupply
,chi_
andnChi
. The two results round down differently when divided by RAY, resulting in two intermediate WAD values. When time differences cause a small difference in chi, the difference in rounding are more significant.diff
) is in WAD precision, and is then immediately used to mint the assets (NST) that remain in the contract by exiting the new debt (in RAD precision) fromnstJoin
(into ERC-20, WAD precision).chi
directly (with RAD precision thatchi * shares
requires). Instead it is adjusted indirectly, via the steps 1 and 2, ignoring any accumulated discrepancy._burn
.Internal pre-conditions
External pre-conditions
Attack Path
Scenario 1 (calculated in PoCs) - Last withdrawer loss:
Scenario 2 - Next depositor loss:
Scenario 3 - ERC-4626 broken methods and views:
convertToAssets
and estimated byconvertToShares
:totalAssets()
is always incorrectmaxRedeem()
/maxWithdraw()
/previewRedeem()
/previewWithdraw()
will be incorrect for last withdrawerredeem
/withdraw
of full balance of the values reported by the views will revert for the last withdrawer.Scenario 4 - overminting and debt inflation:
diff
and their control oftotalSupply
such that indiff = A - B
, B is rounded down, but A is not.vow
s debt (and possibly causeflop
MKR auctions in excesss of actual need).Impact
The last withdrawer (User A in the scenario) suffers a loss of their expected assets, which could be a substantial amount. The PoC shows values within the expected range (
nsr
values up to 20%) that result in shortfall of above 0.5% for final user value of 10K. Specifically, of around $80 in the sample values in the PoC.Because the PoC are simplistic, it's possible that in a different combination of input values and dynamics, larger losses are possible as well.
Additionally, this issue compromises the contract's compliance with the ERC4626 standard, as functions like totalAssets(), maxRedeem(), and maxWithdraw() may return inaccurate values. This could lead to integration issues with other protocols expecting to use this values for withdrawals.
The problem is further exacerbated by:
nsr
values and longer timeframes create much larger discrepancies due to the exponential growth ofchi
.This issue not only causes direct financial loss but also undermines the reliability and predictability of the SNst contract, potentially damaging user trust and protocol reputation.
PoC
Python was used for the PoC due to the need to simulate the maximum amount of drip iterations over a long period of time (this change being termed "endgame").
PoC run results:
Mitigation
vat.dai
) that allows exact correspondence toshares * chi
(in RAD) without division before multiplication. Only use WAD when exiting into the ERC-20 during withdrawal (in_burn
) which stop accumulating thensr
for that NST.chi
should not be tracked. In such an implementation, drip should calculate the mint amount from the available ERC-20 assets using thensr
. This approach can use existing ERC-4626 libraries.