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

MidasRWA: mBTC integration #1232

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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