-
Notifications
You must be signed in to change notification settings - Fork 13
usdai: switch monitoring to pyusd backing and peg checks #199
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
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
cf6b66a
usdai: switch monitoring to pyusd backing and peg checks
ctmotox2 00fffff
added mint monitoring
ctmotox2 7250a8e
usdai: switch large mint monitor to supply delta
ctmotox2 0851be6
usdai: remove liquid pyusd coverage ratio alerts
ctmotox2 2cced72
usdai: monitor totalSupply+bridgedSupply backing invariant
ctmotox2 2e94575
usdai: read large mint supply from latest block
ctmotox2 4cbffca
usdai: address PR199 review feedback
ctmotox2 391ab5f
usdai: move large mint monitor into hourly workflow
ctmotox2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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 |
|---|---|---|
| @@ -1,113 +1,68 @@ | ||
| # USDai Monitoring | ||
|
|
||
| This script monitors the USDai protocol on Arbitrum One, specifically the relationship between its supply and collateral backing. | ||
| USDai monitors on Arbitrum focus on backing safety and loan activity. | ||
|
|
||
| ## Protocol Overview | ||
|
|
||
| - **Docs**: [Proof of Reserves Guide](https://docs.usd.ai/app-guide/proof-of-reserves) | ||
| - **Claimed Backing**: [99.8% by TBills](https://app.usd.ai/reserves) | ||
| - **Mechanism**: USDai is backed by `wM` (Wrapped M) tokens. `M` is a token representing T-Bill yields from the M^0 protocol. | ||
| - **Backing Source**: The M^0 protocol backs `M` tokens with off-chain T-Bills held in custody. `wM` is a wrapper that enables `M` to be used on other chains like Arbitrum, rebasing or accumulating value to reflect T-Bill yield. | ||
| - **Minting**: Minting involves depositing `wM` into the USDai Vault, which ensures 1:1 backing for the minted `USDai`. | ||
| ## Contracts (Arbitrum One) | ||
|
|
||
| ## Metrics & Monitoring | ||
| - USDai Token (proxy): `0x0A1a1A107E45b7Ced86833863f482BC5f4ed82EF` | ||
| - PYUSD Token: `0x46850aD61C2B7d64d08c9C754F45254596696984` | ||
| - sUSDai: `0x0B2b2B2076d95dda7817e785989fE353fe955ef9` | ||
| - Loan Router: `0x0C2ED170F2bB1DF1a44292Ad621B577b3C9597D1` | ||
|
|
||
| We track the following key metrics to ensure solvency and stability: | ||
| ## Backing Invariant | ||
|
|
||
| - **USDai Supply**: The calculated supply based on the `wM` balance held by the USDai Vault on-chain. | ||
| - **Mint Ratio**: The collateralization ratio retrieved from the protocol API (0.995). | ||
| - **Collateral**: Calculated as `USDai Supply / Mint Ratio`. | ||
| - **Buffer**: `Implied Collateral` - `USDai Supply`. A positive buffer indicates the system is functioning within the expected Mint Ratio parameters. | ||
| - **Loans**: Directly fetched from the Loan Router contract. Then calculated `Active loan amounts / total USDai supply`. to calculate the ratio. | ||
| The invariant monitored in `usdai/main.py` is: | ||
|
|
||
| ## Alerts | ||
| `usdai.totalSupply() + usdai.bridgedSupply() <= PYUSD.balanceOf(USDai)` | ||
|
|
||
| - **Buffer Drop**: A Telegram alert is triggered if the **Buffer** value drops below $1M. | ||
| A significant drop could indicate loss of backing value. | ||
| - **Loan Activity**: A Telegram alert is triggered if the **Total Verified Principal** changes (indicating a new loan origination or a repayment). | ||
| - **Legacy Loan Expiry**: A Telegram alert is triggered when the legacy loan (NVIDIA H200s) reaches its maturity date (July 2028). | ||
| All values are normalized to 1e18 units before comparison. | ||
|
|
||
| - **Mint Ratio Change**: A Telegram alert is triggered if the protocol's Mint Ratio changes from its previous value. This is a critical parameter that determines backing requirements. | ||
| - **Governance Events**: We monitor for queued governance actions on the USDai Admin Safe and sUSDai Admin Safe contracts. | ||
| ### Why `bridgedSupply` matters | ||
|
|
||
| ## Contracts (Arbitrum One) | ||
| `bridgedSupply` represents USDai minted for cross-chain/bridge accounting. So the required PYUSD backing is not only local `totalSupply()`, but `totalSupply() + bridgedSupply()`. | ||
|
|
||
| - **USDai Token (Vault)**: `0x0A1a1A107E45b7Ced86833863f482BC5f4ed82EF` | ||
| - **wM Token**: `0x437cc33344a0b27a429f795ff6b469c72698b291` | ||
| - **sUSDai**: `0x0B2b2B2076d95dda7817e785989fE353fe955ef9` | ||
| - **Loan Router**: `0x0C2ED170F2bB1DF1a44292Ad621B577b3C9597D1` | ||
| ### Alert Condition | ||
|
|
||
| ## Tenderly Monitoring | ||
| We alert only when: | ||
|
|
||
| The following addresses and events should be watched via Tenderly alerts: | ||
| `(totalSupply + bridgedSupply - pyusdBalance) >= USDAI_INVARIANT_BREACH_THRESHOLD_RAW` | ||
|
|
||
| 1. **Loan Router** (`0x0C2ED170F2bB1DF1a44292Ad621B577b3C9597D1`): | ||
| - **Transfer**: Monitor for minting/burning of Loan NFTs. | ||
| - **LoanOriginated** (or similar): Monitor for new GPU loan creation. | ||
| Default threshold is `100e18` (100 USDai). | ||
|
|
||
| ## Governance & Security | ||
| ## Loan Monitoring | ||
|
|
||
| - **Access Control**: The USDai token contract implements standard Access Control roles: | ||
| - `DEFAULT_ADMIN_ROLE` (`0x00...`): Can grant and revoke other roles. | ||
| - `hasRole(role, account)`: Used to verify permissions. | ||
| - `getRoleAdmin(role)`: Determines who manages specific roles. | ||
| - **Upgradeability**: | ||
| - **USDai Token**: Is an upgradeable contract (ERC1967Proxy). We monitor for `Upgraded` events. | ||
| - **wM Token**: Is an upgradeable contract (ERC1967Proxy). We monitor for `Upgraded` events. | ||
| - **Functionality**: | ||
| - **Supply Control**: Includes `supplyCap` and `totalSupply`. | ||
| - **Bridging**: Includes `bridgedSupply` and `eip712Domain` (supports cross-chain/permit). | ||
| - **Swap Adapter**: Contains a `swapAdapter` address for integrating swaps or redemptions. | ||
| We also track sUSDai loan principal from Loan Router and alert on meaningful total principal changes. | ||
|
|
||
| ## sUSDai FAQ | ||
| A legacy loan principal is intentionally included as a fixed adjustment for continuity. | ||
|
|
||
| > **How does it work?** | ||
| ## Large Mint Monitoring (No Event Scanning) | ||
|
|
||
| **sUSDai** is a yield-bearing ERC-4626 vault token. It earns yield from M token emissions and by lending USDai to AI infrastructure pools (MetaStreet). It is not a stablecoin but a floating-price token representing a share of the lending portfolio and unallocated cash. | ||
| `usdai/large_mints.py` intentionally does **not** scan events. | ||
|
|
||
| > **How to redeem it? Is there a queue?** | ||
| It runs cached `totalSupply` delta checks and alerts when the increase is above: | ||
|
|
||
| Redemption is done via the [app](https://app.usd.ai/unstake) or directly on-chain. It involves an **asynchronous request with a 30-day queue** (average wait expected to drop to 15 days). Redemptions are processed periodically by the protocol admin. Users wanting instant exit must use secondary markets (DEXs like Fluid/Curve), which currently hold ~$20M liquidity on Arbitrum. | ||
| - `USDAI_LARGE_MINT_THRESHOLD` (default: `100000` USDai) | ||
|
|
||
| > **Can it have losses?** | ||
| The GitHub workflow `.github/workflows/hourly.yml` runs this monitor hourly. | ||
|
|
||
| **Yes.** Unlike USDai (backed by T-Bills), sUSDai carries credit risk from its GPU-backed loans. If loans default and collateral liquidation is insufficient, the share price will drop, leading to principal loss. Redemptions use a "Conservative NAV" (Principal Only) to protect remaining stakers. | ||
| ## Price Monitoring Scope | ||
|
|
||
| > **How is Price Per Share (PPS) defined? On-chain or Off-chain?** | ||
| `usdai/main.py` does not monitor PYUSD/USD price. | ||
|
|
||
| **On-chain calculation using off-chain data.** | ||
| The contract calculates PPS on-chain (see `redemptionSharePrice`), but the underlying Net Asset Value (NAV) relies on off-chain loan health data. The system uses a dual-NAV model: | ||
| Price/peg monitoring should be handled by the shared stable monitor: | ||
|
|
||
| - **Optimistic NAV** (Principal + Interest) for Deposits. | ||
| - **Conservative NAV** (Principal Only) for Redemptions. | ||
| A Chainlink oracle is used to convert pool positions into USDai value. Thus, while you can read the price on-chain, the inputs depend on the strategy's off-chain reporting. | ||
| - `stables/main.py` | ||
|
|
||
| ## Usage | ||
|
|
||
| Collateral/Supply/Loan Monitoring: | ||
| Run USDai invariant + loan monitor: | ||
|
|
||
| ```bash | ||
| uv run usdai/main.py | ||
| ``` | ||
|
|
||
| ### Loan Calculation Methodology | ||
|
|
||
| The script calculates GPU loans by directly scanning the **Loan Router** contract for active loan NFTs held by the sUSDai Vault. | ||
|
|
||
| 1. **Direct Read**: It scans `tokenOfOwnerByIndex` on the Loan Router for the sUSDai address. | ||
| 2. **Decoding**: It decodes the raw `loanState` storage to extract the exact **Principal Amount** and **Maturity Date**. | ||
| 3. **Legacy Loans**: It includes hardcoded values for known legacy loan (NVIDIA H200s, $560k) that originated before the current Loan Router deployment. | ||
| 4. **Total Principal**: Sums these up to track the exact face value of active loans. | ||
|
|
||
| Governance Monitoring: | ||
|
|
||
| We monitor the following Safes for queued transactions using the shared Safe monitoring script: | ||
|
|
||
| - **USDai Admin Safe**: [`0xF223F...`](https://arbiscan.io/address/0xF223F8d92465CfC303B3395fA3A25bfaE02AED51) (2/4 multisig) - Admin of wM Token. | ||
| - **sUSDai Admin Safe**: [`0x783B...`](https://arbiscan.io/address/0x783B08aA21DE056717173f72E04Be0E91328A07b) (3/3 multisig) - Admin of USDai Token (Vault) and sUSDai. | ||
|
|
||
| This runs every 10 minutes via GitHub Actions. | ||
| Run large mint monitor: | ||
|
|
||
| ```bash | ||
| uv run safe/main.py | ||
| uv run usdai/large_mints.py | ||
| ``` |
This file contains hidden or 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,89 @@ | ||
| #!/usr/bin/env python3 | ||
| """Monitor large USDai mints via totalSupply delta only (no event scanning).""" | ||
|
|
||
| from decimal import Decimal, getcontext | ||
|
|
||
| from web3 import Web3 | ||
|
|
||
| from utils.abi import load_abi | ||
| from utils.alert import Alert, AlertSeverity, send_alert | ||
| from utils.cache import cache_filename, get_last_value_for_key_from_file, write_last_value_to_file | ||
| from utils.chains import Chain | ||
| from utils.config import Config | ||
| from utils.logging import get_logger | ||
| from utils.web3_wrapper import ChainManager | ||
|
|
||
| getcontext().prec = 40 | ||
|
|
||
| PROTOCOL = "usdai" | ||
| logger = get_logger(f"{PROTOCOL}.large_mints") | ||
|
|
||
| USDAI_TOKEN_ADDR = Web3.to_checksum_address("0x0A1a1A107E45b7Ced86833863f482BC5f4ed82EF") | ||
|
|
||
| MINT_THRESHOLD_TOKENS = Decimal(Config.get_env("USDAI_LARGE_MINT_THRESHOLD", "100000")) | ||
|
|
||
| CACHE_KEY_LAST_SUPPLY = f"{PROTOCOL}_large_mints_last_supply" | ||
|
|
||
|
|
||
| def _to_int(value) -> int: | ||
| try: | ||
| return int(value) | ||
| except (TypeError, ValueError): | ||
| return 0 | ||
|
|
||
|
|
||
| def _format_units(raw_value: int, decimals: int) -> Decimal: | ||
| return Decimal(raw_value) / (Decimal(10) ** decimals) | ||
|
|
||
|
|
||
| def _send_large_supply_increase_alert(delta_raw: int, previous_raw: int, current_raw: int, decimals: int) -> None: | ||
| delta = _format_units(delta_raw, decimals) | ||
| previous = _format_units(previous_raw, decimals) | ||
| current = _format_units(current_raw, decimals) | ||
|
|
||
| msg = ( | ||
| "*USDai Large Mint Alert (Supply Delta)*\n\n" | ||
| f"Threshold: {MINT_THRESHOLD_TOKENS:,.0f} USDai\n" | ||
| f"Supply increase: {delta:,.2f} USDai\n" | ||
| f"Previous totalSupply: {previous:,.2f}\n" | ||
| f"Current totalSupply: {current:,.2f}\n\n" | ||
| "This monitor intentionally uses only totalSupply deltas (no event scanning)." | ||
| ) | ||
| send_alert(Alert(AlertSeverity.MEDIUM, msg, PROTOCOL)) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| client = ChainManager.get_client(Chain.ARBITRUM) | ||
| erc20_abi = load_abi("common-abi/ERC20.json") | ||
| usdai = client.get_contract(USDAI_TOKEN_ADDR, erc20_abi) | ||
|
|
||
| try: | ||
| with client.batch_requests() as batch: | ||
| batch.add(usdai.functions.decimals()) | ||
| batch.add(usdai.functions.totalSupply()) | ||
| decimals, current_supply_raw = client.execute_batch(batch) | ||
|
|
||
| decimals = int(decimals) | ||
| threshold_raw = int(MINT_THRESHOLD_TOKENS * (Decimal(10) ** decimals)) | ||
| current_supply_raw = int(current_supply_raw) | ||
|
|
||
| last_supply_cached = _to_int(get_last_value_for_key_from_file(cache_filename, CACHE_KEY_LAST_SUPPLY)) | ||
| if last_supply_cached > 0: | ||
| delta_raw = current_supply_raw - last_supply_cached | ||
| if delta_raw >= threshold_raw: | ||
| _send_large_supply_increase_alert( | ||
| delta_raw=delta_raw, | ||
| previous_raw=last_supply_cached, | ||
| current_raw=current_supply_raw, | ||
| decimals=decimals, | ||
| ) | ||
|
|
||
| write_last_value_to_file(cache_filename, CACHE_KEY_LAST_SUPPLY, current_supply_raw) | ||
|
|
||
| except Exception as exc: | ||
| logger.error("USDai large mint monitoring failed: %s", exc) | ||
| send_alert(Alert(AlertSeverity.MEDIUM, f"USDai large mint monitor failed: {exc}", PROTOCOL), plain_text=True) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.