Skip to content

Commit

Permalink
MidasRWA: mBTC integration
Browse files Browse the repository at this point in the history
  • Loading branch information
dmytro-horbatenko committed Dec 17, 2024
1 parent 76d2c6a commit cd4b4d9
Show file tree
Hide file tree
Showing 10 changed files with 989 additions and 0 deletions.
150 changes: 150 additions & 0 deletions contracts/plugins/assets/midas/MidasCollateral.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 "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import { CollateralStatus, ICollateral, IAsset } from "../../../interfaces/IAsset.sol";
import "../../../libraries/Fixed.sol";
import "../AppreciatingFiatCollateral.sol";
import "./interfaces/IMidasDataFeed.sol";
import "./interfaces/IMToken.sol";

/**
* @title MidasCollateral
* @notice A collateral plugin for Midas tokens (mBTC, mTBILL, mBASIS).
*
* ## Scenarios
*
* - mBTC:
* {target}=BTC, {ref}=BTC => {target/ref}=1.
* Need {UoA/target}=USD/BTC from a Chainlink feed.
* Price(UoA/tok) = (USD/BTC)*1*(BTC/mBTC) = USD/mBTC
*
* - mTBILL/mBASIS:
* {target}=USD, {ref}=USDC(=USD) => {target/ref}=1
* {UoA/target}=1 (hardcoded, stable USD)
* Price(UoA/tok)=1*1*(USDC/token)=USD/token
*
* The contract handles both:
* - If targetName="BTC", must provide a USD/BTC feed for {UoA/target}.
* - If targetName="USD", no feed needed; {UoA/target}=1.
*
* ## Behavior
* - Uses IMidasDataFeed for {ref/tok}.
* - For BTC target, uses chainlink feed to get USD/BTC.
* - For USD target, hardcodes {UoA/target}=1, no feed needed.
* - On pause: IFFY then DISABLED after delay.
* - On blacklist: DISABLED immediately.
* - If refPerTok() decreases: DISABLED (handled by AppreciatingFiatCollateral).
*/
contract MidasCollateral is AppreciatingFiatCollateral {
using FixLib for uint192;
using OracleLib for AggregatorV3Interface;

error InvalidTargetName();

bytes32 public constant BLACKLISTED_ROLE = keccak256("BLACKLISTED_ROLE");

IMidasDataFeed public immutable refPerTokFeed;
IMToken public immutable mToken;

AggregatorV3Interface public immutable uoaPerTargetFeed; // {UoA/target}, required if target=BTC
uint48 public immutable uoaPerTargetFeedTimeout; // {s}, only applicable if target=BTC
bytes32 public immutable collateralTargetName;

/**
* @param config CollateralConfig
* @param refPerTokFeed_ IMidasDataFeed for {ref/tok}
* @param revenueHiding (1e-4 for 10 bps)
*/
constructor(
CollateralConfig memory config,
uint192 revenueHiding,
IMidasDataFeed refPerTokFeed_,
uint48 refPerTokTimeout_
) AppreciatingFiatCollateral(config, revenueHiding) {
require(address(refPerTokFeed_) != address(0), "invalid refPerTok feed");

mToken = IMToken(address(config.erc20));
collateralTargetName = config.targetName;
uoaPerTargetFeed = config.chainlinkFeed;
uoaPerTargetFeedTimeout = config.oracleTimeout;
refPerTokFeed = refPerTokFeed_;
}


/// @return {ref/tok}
function underlyingRefPerTok() public view override returns (uint192) {
uint256 rawPrice = refPerTokFeed.getDataInBase18();
if (rawPrice > uint256(FIX_MAX)) revert UIntOutOfBounds();
return uint192(rawPrice);
}

/// @return {target/ref}=1 always (BTC/BTC=1, USD/USDC=1)
function targetPerRef() public pure override returns (uint192) {
return FIX_ONE;
}

/**
* @dev Calculate price(UoA/tok):
* price(UoA/tok) = (UoA/target) * (target/ref) * (ref/tok) = (chainlinkFeed price) * 1 * (underlyingRefPerTok())
*
* For mBTC: {UoA/target}=USD/BTC, refPerTok=BTC/mBTC => USD/mBTC
* For mTBILL/mBASIS as mToken: {UoA/target}=USD/USD=1, refPerTok=USDC/mToken (treated as USD), => USD/mToken
*/
function tryPrice()
external
view
override
returns (
uint192 low,
uint192 high,
uint192 pegPrice
)
{
uint192 uoaPerTarget;
if (collateralTargetName == bytes32("BTC")) {
uoaPerTarget = uoaPerTargetFeed.price(uoaPerTargetFeedTimeout);
} else {
uoaPerTarget = FIX_ONE;
}

uint192 refPerTok_ = underlyingRefPerTok();

uint192 p = uoaPerTarget.mul(refPerTok_);
uint192 err = p.mul(oracleError, CEIL);

low = p - err;
high = p + err;

pegPrice = FIX_ONE;
}

/**
* @dev Checks pause/blacklist state before normal refresh.
* - Blacklisted => DISABLED
* - Paused => IFFY then eventually DISABLED
*/
function refresh() public override {
CollateralStatus oldStatus = status();

if (mToken.accessControl().hasRole(BLACKLISTED_ROLE, address(this))) {
markStatus(CollateralStatus.DISABLED);
} else if (mToken.paused()) {
markStatus(CollateralStatus.IFFY);
} else {
// Attempt to get refPerTok. If this fails, the feed is stale or invalid.
try this.underlyingRefPerTok() returns (uint192 /* refValue */) {
super.refresh();
} catch (bytes memory errData) {
if (errData.length == 0) revert();
markStatus(CollateralStatus.IFFY);
}
}

CollateralStatus newStatus = status();
if (oldStatus != newStatus) {
emit CollateralStatusChanged(oldStatus, newStatus);
}
}
}
153 changes: 153 additions & 0 deletions contracts/plugins/assets/midas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Midas Collateral Plugin (mBTC, mTBILL, mBASIS)

## Overview

This collateral plugin integrates Midas tokens (mBTC, mTBILL, mBASIS) into the Reserve Protocol as collateral. It supports both BTC-based and USD-based targets:

- **mBTC (BTC-based):**
- `{target}=BTC`, `{ref}=BTC`, so `{target/ref}=1`.
- A Chainlink feed provides `{UoA/target}=USD/BTC`.
- `price(UoA/tok) = (USD/BTC)*1*(BTC/mBTC) = USD/mBTC`.

- **mTBILL, mBASIS (USD-based):**
- `{target}=USD`, `{ref}=USDC(≈USD)`, so `{target/ref}=1`.
- Since `{UoA}=USD` and `{target}=USD`, `{UoA/target}=1` directly, no external feed needed.
- `price(UoA/tok)=1*1*(USDC/mToken)=USD/mToken`.

This plugin uses a Midas data feed (`IMidasDataFeed`) to obtain `{ref/tok}`, and leverages `AppreciatingFiatCollateral` to handle revenue hiding and immediate defaults if `refPerTok()` decreases.

### Socials
- Telegram: https://t.me/midasrwa
- Twitter (X): https://x.com/MidasRWA

## Units and Accounting

### mBTC Units

| | Unit |
|------------|---------|
| `{tok}` | mBTC |
| `{ref}` | BTC |
| `{target}` | BTC |
| `{UoA}` | USD |

### mTBILL / mBASIS Units

| | Unit |
|------------|------------------|
| `{tok}` | mTBILL or mBASIS |
| `{ref}` | USDC (≈USD) |
| `{target}` | USD |
| `{UoA}` | USD |


All scenarios: `{target/ref}=1`.

## Key Points

- For mBTC: Requires a Chainlink feed for `{UoA/target}` (USD/BTC).
- For mTBILL/mBASIS: `{UoA/target}=1`, no Chainlink feed needed.
- On pause: transitions collateral to `IFFY` then `DISABLED` after `delayUntilDefault`.
- On blacklist: immediately `DISABLED`.
- If `refPerTok()` ever decreases: immediately `DISABLED`.
- Uses `AppreciatingFiatCollateral` for smoothing small dips in `refPerTok()` (revenue hiding of 10 bps).

## References

The Midas Collateral plugin interacts with several Midas-specific contracts and interfaces

### IMidasDataFeed
- **Purpose**: Provides the `{ref/tok}` exchange rate (scaled to 1e18) for Midas tokens.
- **Usage in Plugin**: The collateral plugin calls `getDataInBase18()` to fetch a stable reference rate.
- **Examples**:
- mBTC Data Feed: [0x9987BE0c1dc5Cd284a4D766f4B5feB4F3cb3E28e](https://etherscan.io/address/0x9987BE0c1dc5Cd284a4D766f4B5feB4F3cb3E28e)
- mTBILL Data Feed: [0xfCEE9754E8C375e145303b7cE7BEca3201734A2B](https://etherscan.io/address/0xfCEE9754E8C375e145303b7cE7BEca3201734A2B)

### IMToken (mBTC, mTBILL)
- **Purpose**: Represents Midas tokens as ERC20 with additional pause/unpause features.
- **Examples**:
- mBTC: [0x007115416AB6c266329a03B09a8aa39aC2eF7d9d](https://etherscan.io/address/0x007115416AB6c266329a03B09a8aa39aC2eF7d9d)
- mTBILL: [0xDD629E5241CbC5919847783e6C96B2De4754e438](https://etherscan.io/address/0xDD629E5241CbC5919847783e6C96B2De4754e438)

## Price Calculation

`price(UoA/tok) = (UoA/target) * (target/ref) * (ref/tok)`

- mBTC: `(UoA/target)=USD/BTC` (from Chainlink), `(ref/tok)=BTC/mBTC``USD/mBTC`.
- mTBILL/mBASIS: `(UoA/target)=1`, `(ref/tok)=USDC/mToken` (≈USD/mToken) → `USD/mToken`.

## Pre-Implementation Q&A

1. **Units:**

- `{tok}`: Midas token
- `{ref}`: mBTC -> BTC, mTBILL/mBASIS -> USDC(≈USD)
- `{target}`: mBTC -> BTC, mTBILL/mBASIS -> USD
- `{UoA}`: USD

2. **Wrapper needed?**
No. Midas tokens are non-rebasing standard ERC-20 tokens. No wrapper is required.

3. **3 Internal Prices:**

- `{ref/tok}` from `IMidasDataFeed`
- `{target/ref}=1`
- `{UoA/target}`:
- mBTC: from Chainlink (USD/BTC)
- mTBILL/mBASIS: 1

4. **Trust Assumptions:**

- Rely on Chainlink feeds for USD/BTC (mBTC case).
- Assume stable `{UoA/target}=1` for USD-based tokens.
- Trust `IMidasDataFeed` for `refPerTok()`.

5. **Protocol-Specific Metrics:**

- Paused => IFFY => DISABLED after delay
- Blacklisted => DISABLED immediately
- `refPerTok()` drop => DISABLED

6. **Unique Abstractions:**

- One contract supports both BTC and USD targets with conditional logic.
- Revenue hiding to smooth tiny dips.

7. **Revenue Hiding Amount:**
A small value like `1e-4` (10 bps) recommended and implemented in constructor parameters.

8. **Rewards Claimable?**
None. Yield is through `refPerTok()` appreciation.

9. **Pre-Refresh Needed?**
No, just `refresh()`.

10. **Price Range <5%?**
Yes, controlled by `oracleError`. For USD tokens, it's trivial. For BTC tokens, depends on Chainlink feed quality.

## Configuration Parameters

When deploying `MidasCollateral` you must provide:

- `CollateralConfig` parameters:
- `priceTimeout`: How long saved prices remain relevant before decaying.
- `chainlinkFeed` (for mBTC): The USD/BTC Chainlink aggregator.
- `oracleError`: Allowed % deviation in oracle price (0.5%).
- `erc20`: The Midas token’s ERC20 address.
- `maxTradeVolume`: Max trade volume in `{UoA}`.
- `oracleTimeout`: Staleness threshold for the `chainlinkFeed`.
- `targetName`: "BTC" or "USD" as bytes32.
- `defaultThreshold`: 0
- `delayUntilDefault`: How long after `IFFY` state to become `DISABLED` without recovery.

- `revenueHiding`: Small fraction to hide revenue (e.g., `1e-4` = 10 bps).
- `refPerTokFeed`: The `IMidasDataFeed` providing `{ref/tok}`.
- `refPerTokTimeout_`: Timeout for `refPerTokFeed` validity (e.g., 30 days).


## Testing

```bash
yarn hardhat test test/plugins/individual-collateral/midas/mbtc.test.ts
yarn hardhat test test/plugins/individual-collateral/midas/mtbill.test.ts
```
46 changes: 46 additions & 0 deletions contracts/plugins/assets/midas/interfaces/IMToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol";

/**
* @title IMToken
* @notice Interface for a Midas token (e.g., mTBILL, mBASIS, mBTC)
*/
interface IMToken is IERC20Upgradeable {
/**
* @notice Returns the MidasAccessControl contract used by this token
* @return The IAccessControlUpgradeable contract instance
*/
function accessControl() external view returns (IAccessControlUpgradeable);

/**
* @notice Returns the pause operator role for mTBILL tokens
* @return The bytes32 role for mTBILL pause operator
*/
function M_TBILL_PAUSE_OPERATOR_ROLE() external view returns (bytes32);

/**
* @notice Returns the pause operator role for mBTC tokens
* @return The bytes32 role for mBTC pause operator
*/
function M_BTC_PAUSE_OPERATOR_ROLE() external view returns (bytes32);

/**
* @notice puts mTBILL token on pause.
* should be called only from permissioned actor
*/
function pause() external;

/**
* @notice puts mTBILL token on pause.
* should be called only from permissioned actor
*/
function unpause() external;

/**
* @dev Returns true if the contract is paused, and false otherwise.
*/
function paused() external view returns (bool);
}
16 changes: 16 additions & 0 deletions contracts/plugins/assets/midas/interfaces/IMidasDataFeed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

interface IMidasDataFeed {
/**
* @notice Fetches the answer from the underlying aggregator and converts it to base18 precision
* @return answer The fetched aggregator answer, scaled to 1e18
*/
function getDataInBase18() external view returns (uint256 answer);

/**
* @notice Returns the role identifier for the feed administrator
* @return The bytes32 role of the feed admin
*/
function feedAdminRole() external view returns (bytes32);
}
12 changes: 12 additions & 0 deletions test/plugins/individual-collateral/midas/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { bn, fp } from '../../../../common/numbers'

// Common constants for tests
export const PRICE_TIMEOUT = bn(604800) // 1 week
export const CHAINLINK_ORACLE_TIMEOUT = bn(86400) // 24 hours
export const MIDAS_ORACLE_TIMEOUT = bn(2592000) // 30 days
export const ORACLE_TIMEOUT_BUFFER = bn(300) // 5 min
export const ORACLE_ERROR = fp('0.005')
export const DEFAULT_THRESHOLD = fp('0')
export const DELAY_UNTIL_DEFAULT = bn(86400) // 24 hours
export const REVENUE_HIDING = fp('0.0001') // 10 bps
export const FORK_BLOCK = 21360000
Loading

0 comments on commit cd4b4d9

Please sign in to comment.