We wanted one number: how far does ADA have to fall before a real share of Indigo's loans gets liquidated? Indigo is a Cardano protocol where you lock ADA and mint synthetic assets (iUSD, iBTC, iETH…) against it. Each position is a CDP, a collateralized debt position.
The protocol's own API was the obvious place to start. When we started (September 9) it listed only the v3 positions: 50 CDPs holding about 6,300 ADA. The real book was about 3,000 times bigger, and it was sitting in plain sight on chain.
This post shows how we read it from a local cardano-db-sync Postgres database, and how we check that the decode is complete. That check matters more than the decode itself.
1. Find where the positions live
On Cardano, a smart contract's funds are UTxOs locked at a script address. Every CDP is one UTxO at Indigo's v2 CDP validator, and that address's payment credential is ff0b10bff20e4b68b491492e5ba6c8048a704763b0a45ce2995da0be.
We found it by working backwards: take recent transactions that mint iUSD and look at which script output each one creates. Every one of them lands on that credential.
2. Pull the unspent outputs and their inline datums
db-sync ships a utxo_view of unspent outputs. Every CDP carries its state as an inline datum, and db-sync stores those already decoded to JSON in the datum table:
SELECT u.value::numeric AS lovelace,
d.value::text AS datum_json
FROM utxo_view u
LEFT JOIN datum d ON d.id = u.inline_datum_id
WHERE u.payment_cred = decode('ff0b10bff20e4b68b491492e5ba6c8048a704763b0a45ce2995da0be', 'hex');
On 2026-10-06 that returned 314 UTxOs. 311 decode as CDPs; the 3 that don't are reported as undecoded.
3. Decode the datum
The v2 CDP datum is nested Plutus constructors. Stripped down:
con0[ con0[ con0[ bytes owner_pkh ],
bytes iasset_name_hex, -- "iUSD", "iBTC", ...
con0[ bytes policy, bytes name ], -- collateral; ("", "") = ADA
int minted_raw, -- the debt, 6 decimals
con0[ int updated_ms, int interest_acc ] ] ]
In Python:
import json
def decode_cdp(lovelace, datum_json):
f = json.loads(datum_json)["fields"][0]["fields"]
asset = bytes.fromhex(f[1]["bytes"]).decode()
if not asset.startswith("i"):
raise ValueError("not a CDP")
return {
"asset": asset,
"owner": f[0]["fields"][0]["bytes"],
"collateral_ada": int(lovelace) / 1e6,
"collateral_is_ada": f[2]["fields"][0]["bytes"] == "",
"debt": int(f[3]["int"]) / 1e6,
}
Anything that doesn't fit the shape gets counted, not dropped. A decoder that quietly skips rows it doesn't understand will tell you the book is smaller than it is, and it will look fine while doing it.
4. Prove it's complete
Every iAsset is minted under one policy (f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880). The total minted minus the total burned is the amount of each iAsset that exists. If our decode has found every position, the decoded debt per asset should equal that number:
SELECT encode(ma.name, 'escape') AS iasset, SUM(mtm.quantity)::numeric AS net_minted
FROM ma_tx_mint mtm JOIN multi_asset ma ON ma.id = mtm.ident
WHERE encode(ma.policy, 'hex') = 'f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880'
GROUP BY 1 ORDER BY 1;
Result on 2026-10-06: iADA, iBTC, iETH, iEUR, iJPY and iSOL reconcile to 100.00%. iUSD comes to 95.2%. We have ruled out the redemption script, which holds reference datums rather than CDPs. We still don't know where the rest is, so we publish the gap rather than round it away, and our pipeline refuses to publish if any asset falls below 85%.
5. The traps
- Liquidation leftovers. 14 UTxOs (on 2026-10-06) hold only the minimum ADA (exactly 1.8619 ADA each) plus a stale debt field, so their ratio looks absurd (one came out at 0.00014). They are positions that were already liquidated. We filter out anything with ≤ 5 ADA and a ratio ≤ 0.5, and report how many we removed.
- No price, no stress test. 6 iSOL positions are left out of the dollar figures because we had no cross-checked USD price for iSOL that run. They still count toward the coverage check above.
- iADA doesn't belong in an ADA-price stress test. Its debt is in ADA, so its collateral ratio doesn't move when the ADA price does. Including it overstates the risk.
-
There is no single minimum ratio to hardcode. The indexer returns
mcr = nullfor every asset, so we show the curve across 110%, 120%, 130% and 150% instead of pretending to know one.
6. What the book looks like
As of 2026-10-06 13:18 UTC, with ADA at $0.276:
| Positions in the stress test | 287 (of 311 decoded) |
| iAsset debt | $1.89M |
| Collateral | 17.4M ADA (~$4.81M) |
| Whole-book collateral ratio | 2.54 |
| ADA price where 10% of the debt is under 120% | $0.175 (−36%) |
| ADA price where 50% of the debt is under 120% | $0.143 (−48%) |
It's a snapshot, not a forecast. The live version, with the full curve and CSVs, is at cardanorecord.com/risk.html, and it's refreshed several times a day from the same node.
We run The Cardano Record, a daily show built entirely from our own node's data, with every prediction it makes scored in public, the misses included. Not financial advice.
Top comments (4)
The mint-minus-burn reconciliation is a smart sanity check, and publishing the iUSD shortfall (95.2% coverage) instead of rounding it away is the right call. Out of curiosity, did you check whether the missing share could sit in positions under an older validator version, or in UTxOs spent between your snapshot and the supply query? Good write-up.
Thanks, and both good questions. I went back and checked.
Older validator versions: Indigo has moved its CDP validator twice (0805d854… → ff0b10bf… in May, → 78c79ef4… on Oct 19). All 314 UTxOs still holding the CDP auth token are at the current validator. The oldest one only holds 9 leftover airdrop UTxOs with no datums, so no positions are hiding there.
Timing between snapshot and supply query: I re-ran the reconciliation as a single SQL statement, so both sides read the same Postgres snapshot (block 14,095,018). iADA, iBTC, iETH, iEUR, iJPY and iSOL still match to the last unit. iUSD is at 96.57%, about 46k iUSD short. So it's not a timing skew. A skew would also hit the other six assets, and it couldn't stay put across weeks of runs.
One thing that looks like a clue: the gap has shrunk over time, from about 109k iUSD in September to 46k now. That fits something old being slowly paid off or moved, rather than an ongoing leak. iUSD's mint history also includes a large share of mint/burn transactions that touch none of the three CDP validators. So the most likely place for the remainder is an earlier contract path we haven't listed yet. I'll update the post if I pin it down. Until then the gap stays published.
Việc truy vấn trực tiếp từ ledger để tính toán rủi ro thanh khoản thay vì dựa vào các API tổng hợp của giao thức là một hướng đi rất thực tế. Khi làm việc với các chain như Cardano, việc phụ thuộc vào indexer của bên thứ ba đôi khi khiến dữ liệu bị trễ hoặc thiếu sót các trạng thái giao dịch trung gian. Cách tiếp cận này giúp đảm bảo tính toàn vẹn của con số mà bạn đang tìm kiếm, đặc biệt là khi tính toán các kịch bản stress test cho tài sản thế chấp. Tuy nhiên, bạn có gặp khó khăn trong việc xử lý khối lượng dữ liệu lớn khi phải parse toàn bộ lịch sử giao dịch để tái cấu trúc lại trạng thái của loan book không?
Cảm ơn bạn! Thực ra chúng tôi không cần parse toàn bộ lịch sử giao dịch. Nhờ mô hình eUTxO, loan book hiện tại chính là tập UTxO chưa chi tiêu đang nằm ở validator CDP. Mỗi vị thế có datum inline chứa khoản nợ, nên chỉ cần một truy vấn lấy khoảng 300 UTxO và giải mã datum là xong, không phải tái dựng trạng thái từ đầu.
Bước duy nhất chạm đến toàn bộ lịch sử là bước đối chiếu: cộng tổng mint trừ burn của từng iAsset. Đó chỉ là một phép tổng trên bảng mint, lọc theo một policy, nên chạy nhanh.
Phần nặng thật sự nằm ở hạ tầng: tự chạy node Cardano cùng cardano-db-sync và Postgres. db-sync cũng là một indexer, nhưng chạy trên node của chính mình, nên không phụ thuộc vào độ trễ hay phần dữ liệu bị lược bỏ của API bên thứ ba. Ngoài ra, cách tìm validator cũng cần cẩn thận: khi Indigo nâng cấp validator (như ngày 19/10), chúng tôi lần theo token xác thực CDP để tìm địa chỉ mới, thay vì cố định một hash.