-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
76d2c6a
commit cd4b4d9
Showing
10 changed files
with
989 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
16
contracts/plugins/assets/midas/interfaces/IMidasDataFeed.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.