Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/hourly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
infinifi/main.py
silo/ur_sniff.py
usdai/main.py
usdai/large_mints.py
yearn/alert_large_flows.py
maple/main.py
timelock/timelock_alerts.py
Expand Down
107 changes: 31 additions & 76 deletions usdai/README.md
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
```
89 changes: 89 additions & 0 deletions usdai/large_mints.py
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

Comment thread
tapired marked this conversation as resolved.
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()
Loading