---
sip: 156
network: Ethereum & Optimism
title: Debt Pool Oracle
status: Rejected
author: 'Kain Warwick (@Kaiynne), Anton Jurisevic (@zyzek)'
created: 2021-07-05T00:00:00.000Z
type: Governance
---

## Simple Summary

<!--"If you can't explain it simply, you don't understand it well enough." Simply describe the outcome the proposed changes intends to achieve. This should be non-technical and accessible to a casual community member.-->

This SIP proposes to replace the existing debt cache mechanism with a debt pool oracle operated by Chainlink.

## Abstract

This SIP will deprecate the existing debt cache mechanism described in SIPs [83](https://sips.synthetix.io/sips/sip-83)
and [91](https://sips.synthetix.io/sips/sip-91) in favour of an oracle that reads the
composition of the debt pool, then calculates the total debt size
off-chain and pushes it on-chain via a Chainlink aggregation contract.

The current debt cache mechanism has the benefit of being entirely on-chain, however, it introduces some
complexity to the protocol. By replacing it with a Chainlink oracle we will simplify several functions and reduce gas costs,
as well as introducing more scalability to the number of Synths the protocol can support, and unifying the
debt pool between chains, which is addressed in [SIP 165](https://sips.synthetix.io/sips/sip-165).

## Motivation

<!--This is the problem statement. This is the *why* of the SIP. It should clearly explain *why* the current state of the protocol is inadequate.  It is critical that you explain *why* the change is needed, if the SIP proposes changing how something is calculated, you must address *why* the current calculation is innaccurate or wrong. This is not the place to describe how the SIP will address the issue!-->

The current debt cache, while an extremely elegant solution to the problem of calculating the size of the debt pool for use by the minting and burning functions has a number of limitations. The primary limitation driving this proposed change is the upcoming need to unify the debt pools across L1 and L2. This requirement would mean that cross chain messaging would need to be enabled and would introduce further complexity to the implementation.
Moving this functionality off-chain will allow for a more scalable network as the number of Synths that can comprise the debt pool will no longer be limited by on-chain computational resources.

## Specification

<!--The specification should describe the syntax and semantics of any new feature, there are five sections
1. Overview
2. Rationale
3. Technical Specification
4. Test Cases
5. Configurable Values
-->

### Overview

<!--This is a high level overview of *how* the SIP will solve the problem. The overview should clearly describe how the new feature will be implemented.-->

1. Implement a new debt pool contract interface to allow the oracle to read and calculate the skew of each Synth.
2. Replace the function to read the debt cache with a new function to read the latest debt oracle value.
3. Add a function to minting and burning that tracks the incremental debt since the last debt oracle update.

### Rationale

<!--This is where you explain the reasoning behind how you propose to solve the problem. Why did you propose to implement the change in this way, what were the considerations and trade-offs. The rationale fleshes out what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work. The rationale may also provide evidence of consensus within the community, and should discuss important objections or concerns raised during discussion.-->

The existing debt system requires snapshots to be performed by an off-chain keeper.
Although this is trustless, in that all the logic required for keeping the debt cache number exists
on chain, it is also relatively expensive. This calculation is
only becoming moreso as new synths and debt-relevant mechanisms such as loans,
the ether wrapper, and futures are added. A debt oracle would need to perform only a single
state update and no extra calculations on-chain, and should therefore be substantially more
efficient to operate. The upshot of the move from an \\(O(n)\\) to an \\(O(1)\\) on-chain component
means that the number of synths the system can support is no longer bottlenecked by the debt snapshot,
but by oracle computations, and so this limit should become substantially higher,
if it is not effectively removed.

At the same time, we were contemplating extending the current debt cache mechanism to support cross network
messaging as well as making a number of other iterative improvements to the calculations.
However, after reviewing the implementation effort compared to performing these calculations
off-chain we concluded that an oracle was the superior solution.

While an oracle increases the reliance on off-chain data aggregation,
the system this SIP proposes is designed to ensure this procedure is as simple as possible,
and processes only easily-accessible on-chain data. We believe this solution has
relatively few failure modes, and therefore a poses a relatively low risk to the system as a whole.

The modularity of this design means that it is easily possible to modify the contract-level
structure of the debt pool calculation that feeds up to the oracle,
so that new protocol functionality can be supported without any modifications to the oracle logic.

### Technical Specification

As the interface between L1 and L2 is to be unified, `DebtCache` and `BaseDebtCache`
should be merged back into a single contract with a substantially modified interface:

```solidity
contract DebtCache {
    function availableDebtComponents() external view returns (bytes32[] memory components);
    function debtComponent(bytes32 key) external view returns (int component, bool invalid);
    function totalDebt() external view returns (int debt, bool invalid, bool stale);
    function totalDebtOnL1() external view returns (int debt, bool invalid, bool stale);
    function totalDebtOnL2() external view returns (int debt, bool invalid, bool stale);
    function issuanceCorrection() external view returns (int correction);
    function updateIssuanceCorrection(int amount, bytes32 synth) external;
    function isInvalidOrStale() external view returns (bool invalid, bool stale);
    function debtOracleStaleTime() external view returns (uint staleTime);
    event DebtUpdated(uint debt);
}
```

The `debtOracleStaleTime` function and `DebtUpdated` event should be preserved from the respective
`debtCacheStaleTime` and `DebtCacheUpdated` members from the previous implementation.
The `isInvalidOrStale` is simply the merger of the previous `isInvalid` and `isStale` functions.
The other contract functions are described below.

#### Debt Components

Currently, the maximum number of components possible in the system is constrained by
the execution budget of the `currentDebt` function. To alleviate this, the
debt system should be able to break down the debt cache into each of its component parts, representing the
net skew for each synth, including both circulating synths (long) and outstanding non-SNX collateral debt (short).
The overall debt pool value will simply be the sum of all these components.

In this way, Chainlink oracle nodes will just need to perform an off-chain sum over all available synth
debt components. The debt pool contract will require new functions to support this:

- `function availableDebtComponents() external view returns (bytes32[] memory)`: Returns the list of all available debt component currency keys (`sUSD`, `sETH`, et cetera)
- `function debtComponent(bytes32 key) external view returns (int component, bool invalid)`: Returns the dollar value of a given component of the debt pool. This value will be positive if the debt component is long-skewed, negative if it is short-skewed.

Each synth's debt component will be calculated as follows:

```solidity
function debtComponent(bytes32 key) external view returns (int component, bool invalid) {
    (uint rate, bool invalid) = ExchangeRates.getRateAndInvalid(key);

    // The circulating supply is the long part of the debt component
    uint component = synth(key).totalSupply();

    // Any non-SNX backed debt is the short part of the debt component
    // Multi-collateral loans
    component -= collateralManager.long(key) + collateralManager.long(short);
    // wrapper debt
    component -= wrapper(key);

    return (component * rate, invalid);
}
```

Note that we have omitted the `EtherCollateral` and `EtherCollateralsUSD` contributions present in the current
implementation, as these are shortly to be deprecated.

#### Oracle Logic

The Chainlink oracle should report the sum of the debt components in a manner equivalent to the following
function:

```javascript
// Implemented off-chain
function currentDebt() {
  debt = 0
  invalid = false

  for (key of DebtPool.availableDebtComponents()) {
    component, (componentInvalid = DebtPool.debtComponent(key))
    debt += component
    invalid |= componentInvalid
  }

  return debt, invalid
}
```

In this way, Synthetix will be able to update the composition of its debt pool calculation by altering the available
debt components and the behaviour of the `debtComponent` function, without Chainlink nodes
needing to update their internal logic. Each debt component will at first correspond to a particular synth,
but in the future may be extended to different objects.

The oracle should simply report the current value returned by this `currentDebt()` function.
Whenever there is a deviation of 1% or more (SCCP configurable) between this result,
and the result returned by `DebtCache.totalDebt()`< then a new update should be pushed on chain.
The oracle should also operate at a regular heartbeat.

#### Total Debt and Issuance Corrections

The updated debt cache contract's interface includes a single new function,
which will report the debt as received from the oracle.

- `function totalDebt() external view returns (uint debt, bool isInvalid)`

This function will replace `Issuer._totalIssuedSynths`, which currently contains a call to
`DebtCache.cacheInfo`. As its name implies, `totalDebt` should report the latest result from the oracle,
and its validity. There is a caveat here, which is that in between oracle updates, it must still accurately
reflect the fluctuations in the debt pool induced by the minting and burning of synths.
it must reflect the resulting fluctuations in the debt pool.
Without this, the issue identified in [SIP-150](https://sips.synthetix.io/sips/sip-150) will remain in place.
To accomplish the desired result, the debt pool contract must track two additional variables:

- `int256 issuanceCorrection`: the net movement in the sUSD value of the debt pool since the last oracle update
- `uint256 lastTimestamp`: the Chainlink debt aggregator update time last known to the cache

With this in mind, the `totalDebt` function should approximately implement the following pseudo-code:

```solidity
function totalDebt() external view returns (uint, bool) {
    // Fetch the latest debt numbers
    (debt, timestamp, isInvalid) = ExchangeRates.debtAndTimestampAndInvalid();

    // An update has occurred, but the new timestamp has not yet been recorded.
    // Do not apply the issuance correction in this case.
    if (lastTimestamp < timestamp) {
        return (debt, isInvalid);
    }

    // Otherwise, account for all mint/burn events
    return (debt + issuanceCorrection, isInvalid, _isStale(timestamp));
}
```

In addition, whenever new synths are minted or burnt, whether against SNX or otherwise, the
sUSD value of those synths should be recorded against the `issuanceCorrection` variable:

```solidity
function updateIssuanceCorrection(int amount, bytes32 synth) external onlyIssuers {
    // Fail if the oracle's debt value is invalid or stale.
    (, timestamp, isInvalid) = ExchangeRates.debtAndTimestampAndInvalid();
    require(!(isInvalid || _isStale(timestamp));

    // If a new update has come through from the oracle, reset the issuance correction
    if (lastTimestamp < timestamp) {
        lastTimestamp = timestamp;
        issuanceCorrection = 0;
    }

    issuanceCorrection += amount * ExchangeRates.getRateAndEnsureValid(synth);
    emit DebtUpdated(totalDebt());
}
```

This function should be invoked whenever synths are created or destroyed, but not when exchanged or transformed into
another form, such as by a deposit into a futures margin account. It must be callable only by synth-issuing contracts
including the issuer, wrapper, and multi-collateral contracts. It must fail if any relevant oracle feeds are invalid.

#### Phase 2 - Total Debt on L1 and L2

The updated debt cache contract's interface includes a two new functions, reporting the `totalDebtOnL1` and `totalDebtOnL2`. based on the total debt on mainnet and L2.

The total debt would be aggregated by Chainlink oracles and reported for both L1 and L2, which will be accessible from the `ExchangeRates` contract on both layers.

- `function totalDebtOnL1() external view returns (int debt, bool invalid, bool stale)`
- `function totalDebtOnL2() external view returns (int debt, bool invalid, bool stale)`

These functions will allow the total combined debt of L1 and L2 debt pools to be calculated within the debt cache contract when debt is issued or burned on both chains.

### Test Cases

<!--Test cases for an implementation are mandatory for SIPs but can be included with the implementation..-->

TBD

### Configurable Values (Via SCCP)

<!--Please list all values configurable via SCCP under this implementation.-->

| Parameter                 | Description                                                                                                                                                | Initial Value |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| Max Debt Oracle Deviation | If the difference between the true system debt and the number reported by the oracle exceeds this value, the oracle should push an updated value on-chain. | 1%            |

## Copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).