Skip to content
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

Demurrage collateral #1218

Closed
wants to merge 41 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d792f85
draft demurrage collateral
tbrent Oct 15, 2024
de9a280
refine
tbrent Oct 15, 2024
2ca1096
impl
tbrent Oct 16, 2024
8721957
paxg tests
tbrent Oct 16, 2024
8f879a7
keep everything else compatible
tbrent Oct 16, 2024
46a6d38
demurrage collateral factory
tbrent Oct 16, 2024
d579601
task for deployment
tbrent Oct 16, 2024
e44e081
cbBTC + EURC deployment scripts (base)
tbrent Oct 16, 2024
39bde26
add missing kw
tbrent Oct 16, 2024
0b1b1da
PAXG mainnet scripts
tbrent Oct 16, 2024
0096d25
scripts and new addresses
tbrent Oct 17, 2024
94a48ff
unit tests
tbrent Oct 17, 2024
1de4dce
fix decay rate constants
tbrent Oct 17, 2024
43daecb
update scripts for full targetName approach
tbrent Oct 17, 2024
7dd6317
init demurrage docs
tbrent Oct 17, 2024
4268cb1
fix target unit naming
tbrent Oct 17, 2024
98fe039
improve test
tbrent Oct 17, 2024
e74e612
GMT
tbrent Oct 17, 2024
9302166
2020 -> 2024
tbrent Oct 17, 2024
9c226ff
fix unix time
tbrent Oct 17, 2024
949e99a
new addresses (base)
tbrent Oct 17, 2024
a816738
fix tests
tbrent Oct 18, 2024
8b224ca
fix tests
tbrent Oct 18, 2024
f80b5a9
deployment + verification scripts for ARB
tbrent Oct 22, 2024
1893208
refactor 100 basis point tier out to prepare for more
tbrent Oct 22, 2024
0a7d58d
docs
tbrent Oct 22, 2024
fe1d6db
record deployments in factory
tbrent Oct 22, 2024
0c0ef5f
fix t0
tbrent Oct 22, 2024
6dbc0e7
fix scenario tests and test multiple rates in basket at once
tbrent Oct 22, 2024
8660b09
lint
tbrent Oct 22, 2024
d06ad18
test revenue processing under even DMR rates
tbrent Oct 22, 2024
106ebdd
fix tests
tbrent Oct 23, 2024
f290330
docs
tbrent Oct 23, 2024
61dc3d2
new base addresses
tbrent Oct 23, 2024
02ec009
verificaton scripts + nits
tbrent Oct 23, 2024
29f2191
fix test
tbrent Oct 25, 2024
5cb9c7e
Merge branch '4.0.0' into demurrage-collateral
tbrent Nov 4, 2024
9a1922d
post merge
tbrent Nov 4, 2024
2259659
fix comet suite
tbrent Nov 4, 2024
9dd8d6d
remove from dep scripts
tbrent Nov 4, 2024
839cf60
nits
tbrent Nov 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,30 @@ export interface ITokens {
// Mountain
USDM?: string
wUSDM?: string

PAXG?: string
cbBTC?: string
EURC?: string
}

export type ITokensKeys = Array<keyof ITokens>

export interface IDemurrageCollateral {
DMR100PAXG?: string
DMR100cbBTC?: string
DMR100EURC?: string
DMR100ARB?: string
}

export interface IFeeds {
stETHETH?: string
stETHUSD?: string
wstETHstETHexr?: string
cbETHETHexr?: string
ETHUSD?: string
wstETHstETH?: string
XAU?: string
EUR?: string
}

export interface IPools {
Expand Down Expand Up @@ -260,6 +273,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
sdUSDCUSDCPlus: '0x9bbF31E99F30c38a5003952206C31EEa77540BeF',
USDe: '0x4c9edd5852cd905f086c759e8383e09bff1e68b3',
sUSDe: '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497',
PAXG: '0x45804880De22913dAFE09f4980848ECE6EcbAf78',
},
chainlinkFeeds: {
RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02',
Expand Down Expand Up @@ -289,6 +303,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
pyUSD: '0x8f1dF6D7F2db73eECE86a18b4381F4707b918FB1',
apxETH: '0x19219BC90F48DeE4d5cF202E09c438FAacFd8Bea', // apxETH/ETH
USDe: '0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961',
XAU: '0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6',
},
AAVE_INCENTIVES: '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5',
AAVE_EMISSIONS_MGR: '0xEE56e2B3D491590B5b31738cC34d5232F378a8D5',
Expand Down Expand Up @@ -517,6 +532,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca',
wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452',
STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df',
cbBTC: '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf',
EURC: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
eUSD: '0xCfA3Ef56d303AE4fAabA0592388F19d7C3399FB4',
meUSD: '0xbb819D845b573B5D7C538F5b85057160cfb5f313',
},
Expand All @@ -535,6 +552,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
eUSD: '0x9b2C948dbA5952A1f5Ab6fA16101c1392b8da1ab', // 0.5%, 24h
},
GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock
Expand Down
29 changes: 29 additions & 0 deletions contracts/facade/factories/DemurrageCollateralFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "../../plugins/assets/DemurrageCollateral.sol";

/**
* @title DemurrageCollateralFactory
*/
contract DemurrageCollateralFactory {
event DemurrageCollateralDeployed(address indexed collateral);

// collateral address => fee per second
mapping(address => uint192) public demurrageDeployments;

bytes32 public constant USD = bytes32("USD");

function deployNewDemurrageCollateral(
CollateralConfig memory config,
DemurrageConfig memory demurrageConfig
) external returns (address newCollateral) {
if (demurrageConfig.isFiat) {
require(config.targetName == USD, "isFiat only compatible with USD");
}

newCollateral = address(new DemurrageCollateral(config, demurrageConfig));
demurrageDeployments[newCollateral] = demurrageConfig.fee;
emit DemurrageCollateralDeployed(newCollateral);
}
}
3 changes: 3 additions & 0 deletions contracts/interfaces/IAsset.sol
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ interface ICollateral is IAsset {

// Used only in Testing. Strictly speaking a Collateral does not need to adhere to this interface
interface TestICollateral is TestIAsset, ICollateral {
/// deprecated
function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh);

/// @return The epoch timestamp when the collateral will default from IFFY to DISABLED
function whenDefault() external view returns (uint256);

Expand Down
150 changes: 150 additions & 0 deletions contracts/plugins/assets/DemurrageCollateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "./FiatCollateral.sol";

struct DemurrageConfig {
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}
uint48 timeout1; // {s}
uint192 error1; // {1}
}

/**
* @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}
*
* under 1 feed:
* - feed0/chainlinkFeed must be {UoA/tok}
* - apply issuance premium IFF isFiat is true
* 2 feeds:
* - feed0: targetUnitFeed0 ? {target/tok} : {UoA/tok}
* - feed1: {UoA/target}
* - apply issuance premium
*
* - tok = Tokenized 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;

uint48 public constant T0 = 1704067200; // {s} Jan 1st 2024 00:00:00 GMT+0000

bool internal immutable isFiat;
bool internal immutable targetUnitFeed0; // if true: feed0 is {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
uint192 public fee; // {1/s} demurrage fee; target unit deflation

/// @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;
feed1 = demurrageConfig.feed1;
timeout0 = config.oracleTimeout;
timeout1 = demurrageConfig.timeout1;
error0 = config.oracleError;
error1 = demurrageConfig.error1;

fee = demurrageConfig.fee;
}

/// Can revert, used by other contract functions in order to catch errors
/// 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/tok} The un-decayed pegPrice
function tryPrice()
external
view
override
returns (
uint192 low,
uint192 high,
uint192 pegPrice
)
{
// 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

pegPrice = FIX_ONE; // undecayed rate that won't trigger default or issuance premium

uint192 x = feed0.price(timeout0); // {UoA/tok}
uint192 xErr = error0;

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/tok} = {UoA/tok} because {target} == {UoA}
pegPrice = x;
}

assert(low <= high);
}

// === Demurrage rates ===

/// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
function refPerTok() public view override returns (uint192) {
// Monotonically increasing due to target unit (and reference unit) deflation

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);
}
}
81 changes: 81 additions & 0 deletions docs/demurrage-collateral.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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 an inflationary reference + target units that refer to a falling quantity of the token unit. This results in a monotonically increasing `refPerTok()` that can be consumed by the protocol to measure appreciation.

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

In principle demurrage can be added to any type of collateral, even already yield-bearing collateral.

**Units**

```solidity
/**
* - tok = Tokenized 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
*/
```

### 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 `1704067200`

### Target Unit (inflationary)

The reference unit maintains a 1:1 rate against the target unit

```
targetPerRef(): 1
```

As a naming convention, we suggest:
`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 the unit absent any demurrage

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.

This also means there can be multiple demurrage collateral for a single token. We refer to these as tiers.

### 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.

This is identical to the calculation for the `refPerTok()` function in the [DemurrageCollateral.sol](../contracts/plugins/assets/DemurrageCollateral.sol) contract, but calculating for an arbitrary timestamp.

```
weight = 1 / (1 - fee) ^ seconds;
```

`fee()` available on DemurrageCollateral contract

### Implementation

[DemurrageCollateral.sol](../contracts/plugins/assets/DemurrageCollateral.sol) implements a generalized demurrage collateral plugin that should support almost all use-cases

Sample usage:

- [deploy_cbbtc_100.ts](../scripts/deployment/phase2-assets/collaterals/deploy_cbbtc_100.ts)
- [deploy_eurc_100.ts](../scripts/deployment/phase2-assets/collaterals/deploy_eurc_100.ts)
- [deploy_paxg_100.ts](../scripts/deployment/phase2-assets/collaterals/deploy_paxg_100.ts)
- [deploy_arb_100.ts](../scripts/deployment/phase2-assets/collaterals/deploy_arb_100.ts)

TODO link to demurrage collateral factory address after deployment
12 changes: 9 additions & 3 deletions scripts/deployment/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import fs from 'fs'
import { ITokens, IComponents, IImplementations, IPools } from '../../common/configuration'
import {
IDemurrageCollateral,
ITokens,
IComponents,
IImplementations,
IPools,
} from '../../common/configuration'

// This file is intended to have minimal imports, so that it can be used from tasks if necessary

Expand Down Expand Up @@ -32,8 +38,8 @@ export interface IDeployments {

export interface IAssetCollDeployments {
assets: ITokens
collateral: ITokens & IPools
erc20s: ITokens & IPools
collateral: ITokens & IPools & IDemurrageCollateral
erc20s: ITokens & IPools & IDemurrageCollateral
}

export interface IRTokenDeployments {
Expand Down
Loading
Loading