SIP 150: Issuance Debt Adjustment Truncation Fix Source

AuthorAnton Jurisevic

Simple Summary

This corrects a potentially-exploitable problem whereby issuer’s debt balances immediately after issuing synths are less than the sUSD they have minted.


Since the EtherWrapper cap was raised, calculation of the excluded debt component of partial debt updates has been interfering with incrementing the cached debt number upon issuance of synths. This SIP resolves the issue by not computing the excluded debt component during sUSD issuance.


The attack surface is limited by the size of a user’s wallet, since the available profit is at most the user’s issued fraction of the debt pool. The burn lock (SIP 40) also makes this difficult to take advantage of in practice. However, if a debt snapshot is not performed quickly enough, an issuer could make free profit by burning their newly-issued sUSD, freeing up all their staked SNX while leaving some sUSD left over.



The DebtCache._updateCachedSynthDebtsWithRates function is called during issuance to update the cached debt with the new synth supply. Due to the way non-SNX-backed debt was being excluded from the cached debt in this function, once the value of non-SNX-backed debt exceeded the circulating sUSD supply the delta being applied truncated to zero. Consequently, the newly-issued synths were effectively invisible to the debt cache, and everyone’s debt balance will be underreported until a fresh snapshot is taken.


The existing logic is incorrect, and this SIP proposes to correct it. It should be noted that as it stands, the offending code will now be completely unused. This is intentional until a more robust pass is made on the debt pool when the L1 and L2 debt pools are unified. This means that the excluded debt component will only be recomputed on full snapshots.

Technical Specification

No changes will be made to the external API, the fix is limited to one internal function only.

Upon issuance, the DebtCache._updateCachedSynthDebtsWithRates function is called to update the contribution of sUSD to the debt pool. During this procedure, code approximately equivalent to the following is executed:

// Compute the change to apply to the cached debt
uint excludedDebt = _cachedSynthDebt[EXCLUDED_DEBT_KEY];
uint cachedSum = _cachedSynthDebt[sUSD].floorsub(excludedDebt);
uint currentSum = sUSD.totalSupply().floorsub(excludedDebt);

// Apply that change
_cachedDebt = _cachedDebt.sub(cachedSum).add(currentSum);

Here, the floorsub function is the same as ordinary subtraction except that it clamps the result to zero if it would be negative. At the time of writing, the circulating sUSD supply is worth around $200 million, while the excluded debt component is worth more than $500 million. Hence the values of cachedSum and currentSum truncate to zero, and the result is that _cachedDebt is not updated. As a result, debt balances underreported until the value of _cachedDebt is updated by a full snapshot. If the newly issued amount of synths is x, then debt balances will be off by a factor about equal to _cachedDebt / (_cachedDebt + x).

The solution proposed in the implementation is to execute no logic related to excludedDebt at all during the issuance process. Not only does this resolve the issue, but it is actually unnecessary to be touching this value at all during issuance.

Test Cases

See the accompanying pull request.

Configurable Values (Via SCCP)


Copyright and related rights waived via CC0.