Skip to content

Commit

Permalink
Merge branch 'demurrage-collateral' into unpriced-collateral
Browse files Browse the repository at this point in the history
  • Loading branch information
tbrent authored Oct 18, 2024
2 parents bae1c19 + 8b224ca commit be4b994
Show file tree
Hide file tree
Showing 16 changed files with 1,090 additions and 55 deletions.
10 changes: 10 additions & 0 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ export interface ITokens {
USDM?: string
wUSDM?: string

// Demurrage collateral
PAXG?: string
cbBTC?: string
EURC?: string
}

export type ITokensKeys = Array<keyof ITokens>
Expand All @@ -130,6 +133,7 @@ export interface IFeeds {
ETHUSD?: string
wstETHstETH?: string
XAU?: string
EUR?: string
}

export interface IPools {
Expand Down Expand Up @@ -519,6 +523,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca',
wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452',
STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df',
cbBTC: '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf',
EURC: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
},
chainlinkFeeds: {
DAI: '0x591e79239a7d679378ec8c847e5038150364c78f', // 0.3%, 24hr
Expand All @@ -535,6 +541,10 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
stETHETH: '0xf586d0728a47229e747d824a939000Cf21dEF5A0', // 0.5%, 24h
ETHUSD: '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70', // 0.15%, 20min
wstETHstETH: '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061', // 0.5%, 24h
BTC: '0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F', // 0.1%, 1200s
cbBTC: '0x07DA0E54543a844a80ABE69c8A12F22B3aA59f9D', // 0.5%, 24h
EURC: '0xDAe398520e2B67cd3f27aeF9Cf14D93D927f8250', // 0.3%, 24h
EUR: '0xc91D87E81faB8f93699ECf7Ee9B44D11e1D53F0F', // 0.3%, 24h
},
GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock
COMET_REWARDS: '0x123964802e6ABabBE1Bc9547D72Ef1B69B00A6b1',
Expand Down
104 changes: 59 additions & 45 deletions contracts/plugins/assets/DemurrageCollateral.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ pragma solidity 0.8.19;
import "./FiatCollateral.sol";

struct DemurrageConfig {
bool isFiat;
uint192 fee; // {1/s} per-second inflation/deflation of refPerTok/targetPerRef
uint192 fee; // {1/s} per-second deflation of the target unit
//
bool isFiat; // if true: {target} == {UoA}
bool targetUnitFeed0; // if true: feed0 is {target/tok}
//
// optional extra feed
AggregatorV3Interface feed1; // empty or {UoA/target}
Expand All @@ -16,54 +18,60 @@ struct DemurrageConfig {
/**
* @title DemurrageCollateral
* @notice Collateral plugin for a genneralized demurrage collateral (i.e /w management fee)
* Warning: Do NOT use the standard targetName() format
* - Use: DMR{annual_demurrage_in_basis_points}{token_symbol}
*
* 1 feed:
* - feed0 {UoA/tok}
* under 1 feed:
* - feed0/chainlinkFeed must be {UoA/tok}
* - apply issuance premium IFF isFiat is true
* 2 feeds:
* - feed0 {target/tok}
* - feed1 {UoA/target}
* - apply issuance premium using feed0
* - feed0: targetUnitFeed0 ? {target/tok} : {UoA/tok}
* - feed1: {UoA/target}
* - apply issuance premium
*
* - tok = Tokenized X
* - ref = Virtual (inflationary) X
* - target = X
* - ref = Decayed X (since 2024-01-01 00:00:00 GMT+0000)
* - target = Decayed X (since 2024-01-01 00:00:00 GMT+0000)
* - UoA = USD
*/
contract DemurrageCollateral is FiatCollateral {
using FixLib for uint192;
using OracleLib for AggregatorV3Interface;

bool internal immutable isFiat;
bool internal immutable targetUnitFeed0; // if true: feed0 is {target/tok}

// For each token, we maintain up to two feeds/timeouts/errors
AggregatorV3Interface internal immutable feed0; // {UoA/tok} or {target/tok}
// up to 2 feeds/timeouts/errors
AggregatorV3Interface internal immutable feed0; // targetUnitFeed0 ? {target/tok} : {UoA/tok}
AggregatorV3Interface internal immutable feed1; // empty or {UoA/target}
uint48 internal immutable timeout0; // {s}
uint48 internal immutable timeout1; // {s}
uint192 internal immutable error0; // {1}
uint192 internal immutable error1; // {1}

// immutable in spirit -- cannot be because of FiatCollateral's targetPerRef() call
// TODO would love to find a way to make these immutable
// TODO would love to find a way to make these immutable for gas reasons
uint48 public t0; // {s} deployment timestamp
uint192 public fee; // {1/s} demurrage fee; manifests as reference unit inflation
uint192 public fee; // {1/s} demurrage fee; target unit deflation

/// @param config.chainlinkFeed unused
/// @param config.oracleTimeout unused
/// @param config.oracleError unused
/// @param demurrageConfig.fee {1/s} fraction of the reference unit to inflate each second
/// @param demurrageConfig.feed0 {UoA/tok} or {target/tok}
/// @param config.chainlinkFeed => feed0: {UoA/tok} or {target/tok}
/// @param config.oracleTimeout => timeout0
/// @param config.oracleError => error0
/// @param demurrageConfig.feed1 empty or {UoA/target}
/// @param demurrageConfig.isFiat true iff {target} == {UoA}
/// @param demurrageConfig.targetUnitfeed0 true iff feed0 is {target/tok} units
/// @param demurrageConfig.fee {1/s} fraction of the target unit to deflate each second
constructor(CollateralConfig memory config, DemurrageConfig memory demurrageConfig)
FiatCollateral(config)
{
isFiat = demurrageConfig.isFiat;
targetUnitFeed0 = demurrageConfig.targetUnitFeed0;

if (demurrageConfig.feed1 != AggregatorV3Interface(address(0))) {
require(demurrageConfig.timeout1 != 0, "missing timeout1");
require(demurrageConfig.error1 > 0 && demurrageConfig.error1 < FIX_ONE, "bad error1");
} else {
require(!demurrageConfig.targetUnitFeed0, "missing UoA info");
}

feed0 = config.chainlinkFeed;
Expand All @@ -81,8 +89,7 @@ contract DemurrageCollateral is FiatCollateral {
/// Should NOT be manipulable by MEV
/// @return low {UoA/tok} The low price estimate
/// @return high {UoA/tok} The high price estimate
/// @return pegPrice {target/ref} The unadjusted price observed in the peg
/// can be 0 if only 1 feed AND not fiat
/// @return pegPrice {target/tok} The un-decayed pegPrice
function tryPrice()
external
view
Expand All @@ -93,45 +100,52 @@ contract DemurrageCollateral is FiatCollateral {
uint192 pegPrice
)
{
pegPrice = FIX_ONE; // undecayed rate that won't trigger default or issuance premium
// This plugin handles pegPrice differently than most -- since FiatCollateral saves
// valid peg ranges at deployment time, they do not account for the decay due to the
// demurrage fee.
//
// The pegPrice should not account for demurrage

// Use only 1 feed if 2nd feed not defined; else multiply together
// if only 1 feed: `y` is FIX_ONE and `yErr` is 0
pegPrice = FIX_ONE; // undecayed rate that won't trigger default or issuance premium

uint192 x = feed0.price(timeout0); // initially {UoA/tok}
uint192 x = feed0.price(timeout0); // {UoA/tok}
uint192 xErr = error0;
uint192 y = FIX_ONE;
uint192 yErr;
if (address(feed1) != address(0)) {
y = feed1.price(timeout1); // {UoA/target}
yErr = error1;

// {target/ref} = {UoA/target}
pegPrice = y; // no demurrage needed
low = x.mul(FIX_ONE - xErr); // {UoA/tok}
high = x.mul(FIX_ONE + xErr); // {UoA/tok}

if (address(feed1) != address(0)) {
if (targetUnitFeed0) {
pegPrice = x; // {target/tok}

uint192 y = feed1.price(timeout1); // {UoA/target}
uint192 yErr = error1;

// Multiply x and y
low = low.mul(y.mul(FIX_ONE - yErr), FLOOR);
high = high.mul(y.mul(FIX_ONE + yErr), CEIL);
} else {
// {target/tok} = {UoA/tok} / {UoA/target}
pegPrice = x.div(feed1.price(timeout1), ROUND);
}
} else if (isFiat) {
// {target/ref} = {UoA/tok}
pegPrice = x; // no demurrage needed
// {target/tok} = {UoA/tok} because {target} == {UoA}
pegPrice = x;
}

// {UoA/tok} = {UoA/ref} * {ref/tok}
low = x.mul(FIX_ONE - xErr).mul(y.mul(FIX_ONE - yErr), FLOOR);
high = x.mul(FIX_ONE + xErr).mul(y.mul(FIX_ONE + yErr), CEIL);
assert(low <= high);
}

// === Demurrage rates ===

// invariant: targetPerRef() * refPerTok() ~= FIX_ONE

/// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
function refPerTok() public view override returns (uint192) {
// up-only
return FIX_ONE.div(targetPerRef(), FLOOR);
}
// Monotonically increasing due to target unit (and reference unit) deflation

/// @return {target/ref} Quantity of whole target units per whole reference unit in the peg
function targetPerRef() public view override returns (uint192) {
// down-only
return FIX_ONE.minus(fee).powu(uint48(block.timestamp - t0));
uint192 denominator = FIX_ONE.minus(fee).powu(uint48(block.timestamp - t0));
if (denominator == 0) return FIX_MAX; // TODO

// up-only
return FIX_ONE.div(denominator, FLOOR);
}
}
56 changes: 56 additions & 0 deletions docs/demurrage-collateral.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Demurrage Collateral Plugins

**Demurrage** is a general term for a per-unit-time fee on assets-under-management (aka management fees)

## Background

Many assets on-chain do not have yield. While the Reserve Protocol is compatible with non-yielding assets, this introduces downsides: an RToken naively composed entirely of non-yielding collateral assets lacks RSR overcollateralization and governance.

In this case a revenue stream can be created by composing a synthetic reference unit that refers to a falling quantity of the collateral token. This causes the reference unit to become inflationary with respect to the collateral unit, resulting in a monotonically increasing `refPerTok()` by definition.

There are side-effects to the `targetName`, however the rest of the collateral plugin remains the same.

### Reference Unit (inflationary)

The reference unit becomes naturally inflationary, resulting in a `refPerTok` of:

```
refPerTok(): 1 / (1 - demurrage_rate_per_second) ^ t
where t is seconds since 01/01/2024 00:00:00 GMT+0000
```

The timestamp of 01/01/2024 00:00:00 GMT+0000 is chosen arbitrarily. It's not important what this value is, but there are benefits to using a common anchor (and 1970 is wastefully far).

In unix time this is `1704085200`

### Target Unit

```
targetPerRef(): 1
```

`DMR{annual_demurrage_in_basis_points}{token_symbol}` or `DMR100USD`, for example

1. The `DMR` prefix is short for demurrage
2. The `annual_demurrage_in_basis_points` is a number such as 100 for 1% annually
3. The `token_symbol` is the symbol of what would have otherwise been the target unit had the collateral been purely SelfReferential

Collateral can only be automatically substituted in the basket with collateral that share the _exact_ same target unit. This unfortunately means a standard WETH collateral cannot be backup for a demurrage ETH collateral. Both the unit type and rate must be identical in order for two collateral to be in the same target unit class.

### Setting the basket weights

Prime basket weights are in units of January 1st 2024 collateral, not today's collateral. It doesn't matter if the collateral wasn't around in Jan 2024 -- when setting the basket weights the setter must take into account how much demurrage has occurred since January 1st 2024.

For example, say an asset has had 2% total demurrage since January 1st 2024 and you want to (on today's date) create a basket of that is worth $1: the correct basket weight to provide to `setPrimeBasket()` would be `1 / (1 - 0.02) = ~1.0204`.

To calculate total demurrage since 2024-01-01 00:00:00 UTC, use:

```
fee() ^ (seconds_since_2024_01_01)
```

(where `fee()` is the per-second demurrage rate found on the `DemurrageCollateral` contract below)

### Implementation

[DemurrageCollateral.sol](../contracts/plugins/assets/DemurrageCollateral.sol) implements a generalized demurrage collateral plugin that should support almost all use-cases
10 changes: 7 additions & 3 deletions scripts/addresses/8453-tmp-assets-collateral.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"cbETH": "0x851B461a9744f4c9E996C03072cAB6f44Fa04d0D",
"saBasUSDC": "0xC19f5d60e2Aca1174f3D5Fe189f0A69afaB76f50",
"cUSDCv3": "0xf7a9D27c3B60c78c6F6e2c2d6ED6E8B94b352461",
"wstETH": "0x8b4374005291B8FCD14C4E947604b2FB3C660A73"
"wstETH": "0x8b4374005291B8FCD14C4E947604b2FB3C660A73",
"EURC": "0x7321485aA1D0439296B882bBc85Eb0BD350F8381",
"cbBTC": "0x06f7D10f5842fc5816DF9A9DD65f84481B1490E3"
},
"erc20s": {
"COMP": "0x9e1028F5F1D5eDE59748FFceE5532509976840E0",
Expand All @@ -23,6 +25,8 @@
"saBasUSDC": "0x6F6f81e5E66f503184f2202D83a79650c3285759",
"STG": "0xE3B53AF74a4BF62Ae5511055290838050bf764Df",
"cUSDCv3": "0x53f1Df4E5591Ae35Bf738742981669c3767241FA",
"wstETH": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452"
"wstETH": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
"EURC": "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42",
"cbBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"
}
}
}
5 changes: 4 additions & 1 deletion scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ async function main() {
'phase2-assets/collaterals/deploy_USDe.ts',
'phase2-assets/assets/deploy_crv.ts',
'phase2-assets/assets/deploy_cvx.ts',
'phase2-assets/collaterals/deploy_pyusd.ts'
'phase2-assets/collaterals/deploy_pyusd.ts',
'phase2-assets/collaterals/deploy_paxg.ts'
)
} else if (chainId == '8453' || chainId == '84531') {
// Base L2 chains
Expand All @@ -103,6 +104,8 @@ async function main() {
'phase2-assets/collaterals/deploy_aave_v3_usdc.ts',
'phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts',
'phase2-assets/collaterals/deploy_cbeth_collateral.ts',
'phase2-assets/collaterals/deploy_cbbtc.ts',
'phase2-assets/collaterals/deploy_eurc.ts',
'phase2-assets/assets/deploy_stg.ts'
)
} else if (chainId == '42161' || chainId == '421614') {
Expand Down
Loading

0 comments on commit be4b994

Please sign in to comment.