Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
cache
out
.certora_internal
sql/test/__pycache__/
sql/test/events/
sql/test/expected_state.json

2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ optimizer = true
optimizer_runs = 800
bytecode_hash = "none"
evm_version = "osaka"
fs_permissions = [{ access = "read", path = "test/ticks_exact.json" }]
fs_permissions = [{ access = "read", path = "test/ticks_exact.json" }, { access = "read-write", path = "sql/test/" }]

[profile.default.fmt]
wrap_comments = true
Expand Down
60 changes: 60 additions & 0 deletions sql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Midnight SQL Queries

Each `.sql` file reconstructs one piece of Midnight's on-chain state from events alone, targeting **Dune Analytics** (Trino SQL with native `uint256`). The queries are self-contained — no off-chain storage is assumed.

## Queries

| File | State variable reconstructed |
|---|---|
| `position.sql` | `position[id][user]` — credit, debt, pendingFee, lastLossFactor, lastAccrual |
| `position_collateral.sql` | `position[id][user].collateral[index]` + collateralBitmap |
| `market_state.sql` | `marketState[id]` — all fields including continuousFeeCredit (recursive CTE) |
| `consumed.sql` | `consumed[user][group]` |
| `is_authorized.sql` | `isAuthorized[authorizer][authorized]` |
| `claimable_settlement_fee.sql` | `claimableSettlementFee[token]` |
| `default_settlement_fee.sql` | `defaultSettlementFeeCbp[loanToken][index]` |
| `default_continuous_fee.sql` | `defaultContinuousFee[loanToken]` |
| `roles.sql` | `roleSetter`, `feeSetter`, `feeClaimer`, `tickSpacingSetter` |

Dune table names follow the pattern `midnight.midnight_evt_<eventname>` (all lowercase).

## Testing

The test harness verifies every query against a live Foundry scenario:

1. **`test/SqlScenarioTest.sol`** deploys Midnight, runs a randomised sequence of operations (takes, repays, liquidations, fee claims, …), captures all emitted events via `vm.recordLogs()`, and writes two artefacts:
- `sql/test/events/<eventname>.json` — one JSON array per event type, used as the SQL input tables
- `sql/test/expected_state.json` — the actual contract state read at the end, used as ground truth

2. **`test/verify.py`** loads the event JSON files into an in-memory DuckDB instance, executes each `.sql` file against them (adapting Trino-specific syntax on the fly), and asserts that the results match `expected_state.json`.

### Run the tests

```bash
bash sql/test/run.sh
```

This runs the Forge scenario test then the Python verifier in one step. [uv](https://github.com/astral-sh/uv) is required; it installs the pinned Python dependencies automatically.

### Dependencies

Python dependencies are pinned in `sql/test/requirements.txt`:

```
duckdb==1.5.3
pandas==3.0.3
```

The DuckDB version matters: `UHUGEINT` values (used for `uint128` fields such as `lossFactor`) must be returned as exact Python integers, which requires DuckDB ≥ 1.5.

### How the SQL adaptation works

`verify.py` translates Trino SQL to DuckDB SQL on the fly (`adapt_sql`):

- Table references `midnight.midnight_evt_*` → bare table names
- `UINT256 '…'` / `BIGINT '…'` literals → plain integer literals
- `CAST(… AS uint256)` → `CAST(… AS BIGINT)`
- `MAX_BY(x, ROW(block, idx))` → `arg_max(x, block * 1e9 + idx)`
- `WITH` → `WITH RECURSIVE` (DuckDB requires the keyword; Trino infers it)
- The `MAX_U128` sentinel (`2¹²⁸ − 1`) → `…::UHUGEINT` so that CFC arithmetic does not truncate
- The `cfc_state` recursive CTE seed row is rewritten to initialize `cfc` and `prev_lf` as `UHUGEINT` so DuckDB preserves the type throughout the recursion
28 changes: 28 additions & 0 deletions sql/claimable_settlement_fee.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- Reconstructs claimableSettlementFee[token] from events.
-- Take: claimableSettlementFee[loanToken] += (buyerAssets - sellerAssets)
-- ClaimSettlementFee: claimableSettlementFee[token] -= amount
-- Platform: Dune Analytics (Trino SQL, native uint256)

WITH

market_loan_tokens AS (
SELECT id_, market_loantoken AS loan_token
FROM midnight.midnight_evt_marketcreated
),

deltas AS (
-- Settlement fee accrues on every Take: buyer pays more than seller receives
SELECT mlt.loan_token AS token, t.buyerassets - t.sellerassets AS add_, UINT256 '0' AS sub_
FROM midnight.midnight_evt_take t
JOIN market_loan_tokens mlt ON mlt.id_ = t.id_

UNION ALL

-- Fee claimer withdraws accumulated fees
SELECT token, UINT256 '0' AS add_, amount AS sub_
FROM midnight.midnight_evt_claimsettlementfee
)

SELECT token, SUM(add_) - SUM(sub_) AS claimable_settlement_fee
FROM deltas
GROUP BY token
45 changes: 45 additions & 0 deletions sql/consumed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- Reconstructs consumed[user][group] from events.
-- Both Take and SetConsumed emit the ABSOLUTE current value (not a delta).
-- Returns the last value per (user, group).
-- Platform: Dune Analytics (Trino SQL, native uint256)
--
-- In Take: consumed[offer.maker][offer.group] = consumed field (newConsumed after increment)
-- In SetConsumed: consumed[onBehalf][group] = amount (monotone set, may skip to max to cancel)

WITH

all_consumed_updates AS (
-- From Take: the offer's maker consumed amount is updated
SELECT
maker AS user,
"group" AS "group",
consumed AS amount,
evt_block_number,
evt_index
FROM midnight.midnight_evt_take

UNION ALL

-- From SetConsumed: explicit set (e.g. to cancel all offers in a group)
SELECT
onbehalf AS user,
"group" AS "group",
amount AS amount,
evt_block_number,
evt_index
FROM midnight.midnight_evt_setconsumed
)

SELECT user, "group", amount
FROM (
SELECT
user,
"group",
amount,
ROW_NUMBER() OVER (
PARTITION BY user, "group"
ORDER BY evt_block_number DESC, evt_index DESC
) AS rn
FROM all_consumed_updates
)
WHERE rn = 1
16 changes: 16 additions & 0 deletions sql/default_continuous_fee.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Reconstructs defaultContinuousFee[loanToken] from events.
-- SetDefaultContinuousFee emits the new value; last value per loanToken wins.
-- Platform: Dune Analytics (Trino SQL, native uint256)

SELECT loantoken AS loan_token, fee
FROM (
SELECT
loantoken,
newcontinuousfee AS fee,
ROW_NUMBER() OVER (
PARTITION BY loantoken
ORDER BY evt_block_number DESC, evt_index DESC
) AS rn
FROM midnight.midnight_evt_setdefaultcontinuousfee
)
WHERE rn = 1
17 changes: 17 additions & 0 deletions sql/default_settlement_fee.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Reconstructs defaultSettlementFeeCbp[loanToken][index] from events.
-- SetDefaultSettlementFee emits the new value; last value per (loanToken, index) wins.
-- Platform: Dune Analytics (Trino SQL, native uint256)

SELECT loantoken AS loan_token, index, fee_cbp
FROM (
SELECT
loantoken,
index,
newsettlementfee / 1000000000000 AS fee_cbp,
ROW_NUMBER() OVER (
PARTITION BY loantoken, index
ORDER BY evt_block_number DESC, evt_index DESC
) AS rn
FROM midnight.midnight_evt_setdefaultsettlementfee
)
WHERE rn = 1
17 changes: 17 additions & 0 deletions sql/is_authorized.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Reconstructs isAuthorized[authorizer][authorized] from events.
-- Returns the last newIsAuthorized value per (authorizer, authorized) pair.
-- Platform: Dune Analytics (Trino SQL)

SELECT authorizer, authorized, is_authorized
FROM (
SELECT
onbehalf AS authorizer,
authorized AS authorized,
newisauthorized AS is_authorized,
ROW_NUMBER() OVER (
PARTITION BY onbehalf, authorized
ORDER BY evt_block_number DESC, evt_index DESC
) AS rn
FROM midnight.midnight_evt_setisauthorized
)
WHERE rn = 1
Loading
Loading