# Zeitfi Backend & Contracts Roadmap ## MVP constraints - Only us as vault managers. - No automation yet. No liquidation engine. - All rolls are triggered manually. We manually ensure prefunding. - Windowing policy is flexible, not constrained by code. We aim at daily rolls but solver should not force us to. - All withdraw requests are accepted immediately. - Selections for roll can be made manually on UI when triggering a roll. ### KNOWN BUGS / MISSING FUNCTIONALITY 1 - [ ] Logs should always reference a vault ID, for debugging from remote server - [ ] Manager UI : Recover Roll button is disable in the state where it's halted on "advance" - [ ] Double spend erros for limit order: position entries & exits: multiple concurrent ones possible; detect failed executions due to **strategy balance sharing** (do we want to ensure that?) - [ ] Double spend errors for FAK orders: position entries & exits outside of liquidation: multiple concurrent ones possible; issues due to **delayed on-chain settlement**: both orders accepted by API and "matched", but trades are FAILED for the second one due to insufficient funds. - mitigate via **one-order-at-a-time** for market orders (position entries & exits outside of liquidations) - [ ] realizable NAV calculation not fully accurate - [ ] include closed market position values (`1 USDC` or `0 USDC` per share) - [ ] Defend against any failed http request in scenarios where important updates are made / fail to be made based on the data received - [ ] avoid fallbacks on unreasonable values - [ ] ensure state can be recovered - [ ] Max number of positions a vault can hold - including the bot - consider in query for closed positions - paginate if necessary - [ ] We rely on successful ingestion of manual and auto liquidations orders as liquidation snapshots for later reference when grouping trades fetched from the API - what's affected if something is missing? - [ ] We rely on updating the prefundRequirement after every manual or auto liquidation is inserted into the corresponding array from roll.preparation - [ ] should add update on claims too - [ ] what happens if prefundRequirement is not updated: where do we rely on it during autoliquidation and can we avoid relying on it? - [ ] Exit partially, then let resolve, then claim => is the redemption value calculation correct? Is the `totalBought` of the closed position as expected? - [ ] Use Activity instead of Closed Positions (details below) - [ ] cover zero-supply retirement case w/ total auto liquidations ### COMPLETE ROLL - [ ] use 30m time pre-roll for high certainty on status of existing positions (+STATE MACHINE integration for recovery) - [ ] no new actions allowed (order / redeem) - right now there is a lock on proxying orders and it looks for existing latest row with pp_rolls.progress != 'finalized'; we allow no BUYs during a roll and only SELLs / redemptions during manual liquidation phase - [ ] ensure market orders are matched and associate trades "CONFIRMED" - [ ] ensure redemptions are confirmed and reflected in API data - [ ] on polymarket API `/activity` we find the `"type" : "REDEEM"` item and the strategy safe must have 0 balance of the CTF tokens associated - [ ] ensure redeemable positions are redeemed - [ ] cancel all limit orders - [ ] Roll automation: use CRONs to roll every 24 hs according to windowing policy and vault manager settings (+STATE MACHINE integration for recovery) - [ ] Roll triggers and roll resume triggers: vault manager should have the request authorized on the backend too - [ ] Stuck in roll because positions have become too small to perform exits (cannot be predicted / prevented; roll has already commited to a certain selection -> may have to undo it) - [ ] lock on vault during roll (no other roll triggered) - [ ] basic recovery routine after backend crash: identify vaults with crash-halted rolls (reliably) and resume their rolls while a lock is applied there - [ ] Revisit roll robustness (zero-shares retirement, zero-nav retirement, stability on roll halt) - [ ] Halt due to missing-CLOB-and-cannot-redeem: attempt to set trigger for a retry? -> depends on halt reason: either liquidity is too thin, or trading is over entirely; on thin liquidity, try later; on trading over entirely, depends on why trading is halted: if resolution soon, attempt try later; - [ ] Revisit lock on trades: no entries during roll; no exits outside of manual liquidation phase; - [ ] Revisit all Date calculations - dates from DB are strings in local time (add Z and then convert) - [ ] Closed position doesn't immediately appear in results after a successful claim. Mitigate during liquidations (getting error when displaying progress in Manage Liquidations) - [ ] Claim of redeemable from manage liquidations buggy - wrong indexSet (see below) ### REDISCUSS / RECONSIDER - [ ] Support zero payout for withdrawals (edge case) - [x] backend creates zero cash claims - [ ] UI should display these without claim transaction button (just info and ack) - [ ] BACKEND UPDATE -> new ABIs breaking current vaults ### ROLL MANAGEMENT - [x] endpoint for latest-roll (UI can suggest what to do next) - [x] liquidation engine / vault manager prefunding guidance (see below in valuation policy) - [ ] Resume UX on failed roll - Vault Manager's view on a halted roll sad paths (see below) - [ ] Regular Vault Manager -> sees halted status - [ ] Admin -> can trigger the roll to resume / repeat ### KNOWN BUGS / MISSING FUNCTIONALITY 2 - [ ] FeeManager calculation (manager performance fee too small) - after "valuation" 👆 - [ ] Fix finality check - after "roll management" 👆 - [ ] backend calc of final performance fee (too low) - not critical for v0, only when we'll retire ### TESTING Test Calculation on a few static examples with simple numbers - share issuance (fork tests in sc repo) - redemption amounts (fork tests in sc repo) - fee correctness (shares on regular / usdc on retirement - fork tests in sc repo) - liquidation amounts (backend prefunding) Test Selections - should be possible to skip (fork tests in sc repo) - should be gapless (fork tests in sc repo, backend tests in context of special roll preparation) ### SECURITY - protection against unwanted deposits: skimming function (public), call regularly - protection against silly deployments etc. (multiple approvals or smth.) - ensure no private data can leak (private keys, not log anything (GH / logs)) ### ROBUSTNESS 2 (LOW PRIO) - [ ] run a completely empty roll (admin route only, no UI) - [ ] admin route to ensure all requests events were ingested ? ### POLISH - [ ] UI for market orders: est. shares obtained in BUY & est. USDC obtained in SELL -> simulate market order, don't do based on best price - [ ] check comments from public facing functions - they appear on polygonscan - [ ] save all deployment txs in vault metadata - [ ] robust verification for all deployments, even if concurrent - [ ] claims should appear on UI after roll is finished, not while it's still happening (ensure the requests are already solved) - [ ] test retirement - safe transfer for prefunding takes unusually long - [ ] refactor: move all db queries from top level route handler functions into separate files - [ ] avoid need for keychain on git pulls with zeitfi-vault:sync in polyproxy ### SCALABILITY - [ ] need to use jsonb instead of json in DB? ## Deferred to v1 (not for mvp) ### ROBUSTNESS - [ ] ensure events are picked up by the webhooks (see below, "Requirements / Design" for details) - [ ] Roll recovery in case of crash, error handling, error tracking (see details below) - thorough testing of all cases ## Requirements / Design ### Non-Tradable Positions #### Redeemable Position (Defined) - no more trading - can redeem immediately - normal occurence **we must account for** in the UI display and roll handling This state is **FINAL**, we can **easily decide how to act in code** (see details in section below) #### Illiquid Position (Defined) - thin orderbook - **partially illiquid**: market has insufficient bids to fill - can occur more often - empty orderbook (no bids) - **100% illiquid** no bids at all anymore - how likely? - *limbo* - market closed and no redemptions yet - **100% illiquid** - not clear how often it happens and how long it takes; depends on resolution & dispute mechanism, as well as the nature of the event. All illiquid states are **NON-FINAL**, and "anything can happen". It's **difficult to decide in code** when to expect a return to liquidity (cases 1 & 2) or a transition to "redeemable" (case 3). Could be within minutes, hours, days, months. #### HANDLING Redeemable Positions Redeemable Positions should: - be auto redeemed before liquidation terms are defined - in cases when the position was tradable at roll start but became redeem-only during liquidations: - for manual liquidation purposes, it should appear different in the "Manage Liquidations" dialog - display still in the table with exitable positions, along with others, but - have a "Trading Stopped" label - have a "Redeem" button instead of "Exit" - for auto liquidation purposes, it should be dected and exited first thing before performing any auto liquidation exit; - after redemption, whether manual or auto, the position should - be counted as a pro-rata exit overshot, i.e. as an exit 100% of the position, beyond the pro-rata suggestion - be displayed with a "Redeemed" label in the Proceed Roll and Liquidation History summary view (table with position exits progress) #### HANDLING Illiquid Positions (total or partial) Include `illiquidPositions` in vault nav calculation, for frontend consumption. We want to save detected illiquid positions in `pp_user_vaults.illiquid_positions` . We may detect that when we: - create a vault snapshot with `createVaultSnapshot()` in @packages/vault-analytics/src/snapshots/create-snapshot.ts - perform a nav measurement during roll planning, see `computeLiquidationTerms()` in @packages/solver/src/workflow-steps/02-plan-roll.ts - perform auto liquidations in `autoLiquidateIfNecessary()` in @packages/solver/src/liquidation-executor.ts The values from `illiquid_positions` will impact workflows: - start of a roll (general stabilization window at start) - liquidation terms definition of a roll - auto-liquidation In all cases, we may do one of the following: - reject with a pp_rolls.error and assume manual retriggering later - postpone via cron - retry within a couple of minutes, until fallback on one of the previous options ### Roll Management w/ forced liquidation #### Valuation on Roll Start CLOB based valuation (all positions that strategy holds). All **settlement of withdrawals** occurs against the NAV measured at the moment of the roll start. If additional cash is required, the liquidation phase must reach either of the targets: 1. usdc 2. pro-rata position sizes #### Iteration 1 - no automation The roll can be triggered at any moment by the admin/manager. Liquidation phase has a **manual liquidation sub-phase** and a **forced liquidation sub-phase**. The manual sub-phase can take arbitrarily long. The manual sub-phase can end when the manager clicks "proceed": we check if any of the liquidation targets were met. If yes, the roll proceeds. If no, we enter forced liquidation. During the complete liquidation phase, the manager can enter no positions. During the forced liquidation sub-phase, the manager can neither enter or exit positions. ##### Windowing The manager selects what to include in each roll, no windowing strategy is imposed or suggested by the system. #### Iteration 2 - windowing based automation The roll triggers every day at a given time. If the liquidation phase is entered, a single-time cron is set up to trigger `x minutes` later. Before the second cron triggers, the manager can liquidate manually and click "proceed". We check if any of the targets is met. If yes, we proceed. If no, we warn the user about proceeding. On cancel, we wait. On confirmation, we proceed to forced liquidations. If the manager doesn't trigger the roll proceed, then the second cron trigger will. On that trigger we check if any of the targets is met. If yes, the roll continues. If not, the forced liquidation starts. During the complete liquidation phase, the manager can enter no positions. During the forced liquidation sub-phase, the manager can neither enter or exit positions. ##### Windowing We want a windowing policy with "deposits every time, withdrawals every n-th time". We also consider to keep allowing the manager to manually select what to include in each roll (as it is now). **TBD** how to match these 2 requirements so that they don't conflict. To cover the **no-manager-engagement** scenario, we say: if it's the time to do a deposit-only roll, we select all pending deposits; if it's the time to do a full roll, we select all pending deposits & withdrwals. To cover the **windowing-policy-violating-engagement** scenario: we say: **TBD** Timing: every day we roll at an exact time, all vaults in sequence. Alternating: select window for withdrawals, but not later than max possible. NegRisk Market: Buy almost all for less than 1 USD, win 1 USD ### Roll Robustness Test all roll workflow failure cases - reliably expose logs from script execution in real-time (crash scenario) - error handling - tx not sent in foundry script - revert in foundry script - error in bash script execution -> need to capture the specific error? (go with tee) - error higher up => persisting the error: local file and DB: keep the most recent error in pp_rolls.error TEXT ! ensure the bash script deletes the output file before running forge and generating the output file - job progress on sent but unconfirmed txs: avoid repeating txs; even if we have idempotency achieved by double prevention, we need to confirm success of the initial, actually successful tx - still need reliable tracking for that. - ability to resume failed roll in state machine (from all possible steps) - final roll net pps needs to be set separately if that step is skipped - tx failed due to little gas -> notification -> manual retrigger after top up ### Contract verification after deployment - multiple deployments in parallel -> verification may fail due to overlaps of cache/Deploy.s.sol/137/run-latest.json ### Robust Webhooks From the Alchemy docs: >Automatic Retries - Webhooks have built-in retry-logic with exponential backoff for non-200 response codes in failures to reach your server. For Free and PAYG tier teams, these will fire with back off up to 10 minutes after. For Enterprise customers, this back off period will extend to 1 hour. Manual Retries (Coming Soon) - Soon you’ll have the ability to manually trigger retries for failed events! #### Solution Before the roll we can: - check epoch event on-chain - patch events table - run roll with patched table ### Sub-Unit Amounts 1. In case of a very high PPS, a deposited amount can result in 0 shares. 2. In case of a very small PPS, a withdrawn amount can result in 0 USDC. #### Small PPS Examples - `pps = 10 ^ 6 * 10 ^ 18` (1 million) - `deposited = 1 USDC` i.e. the UI enforced minimum - `supply = 100 * 10 ^ 18` - resulting `sharesMinted = deposited(1e6) * (1e12 * 1e18) / pps` `sharesMinted = 1000000000000` => This would only be an issue with a pps of 10 ^ 12 * 1 million ✅ ### Wrong index set #### Position ``` { "asset": "95597884330666681363220929540793785663686221789914793407818360390571554923138", "conditionId": "0x4799739a5074388ab70358c868a53df90136ff41b2bd31343b999dc4fea41305", "size": "1108900", "recommendedExit": "560000", "price": "0.855", "slug": "btc-updown-15m-1770307200", "outcome": "Down", "eventId": "198511", "endDate": "2026-02-05", "redeemable": false, "illiquid": false, "positionState": { "closed": true, "closedPosition": { "proxyWallet": "0xa8fd12cd9ef27bb5048339aa833d195926dfd532", "asset": "95597884330666681363220929540793785663686221789914793407818360390571554923138", "conditionId": "0x4799739a5074388ab70358c868a53df90136ff41b2bd31343b999dc4fea41305", "avgPrice": 0.9, "totalBought": 1.11111, "realizedPnl": 0.111111, "curPrice": 1, "title": "Bitcoin Up or Down - February 5, 11:00AM-11:15AM ET", "slug": "btc-updown-15m-1770307200", "icon": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png", "eventSlug": "btc-updown-15m-1770307200", "outcome": "Down", "outcomeIndex": 1, "oppositeOutcome": "Up", "oppositeAsset": "73183898795076957940662770274765175274868069895547643417263396300546644692811", "endDate": "2026-02-05T00:00:00Z", "timestamp": 1770319824 }, "marketClosed": true, "redeemable": false, "illiquid": false, "isWinner": true }, "filledSize": "1.11111", "filledPct": 100, "confirmedProceeds": "1.11111" } ``` #### Case logs ##### EXECUTION FROM MANAGE-LIQUIDATION POSITION EXIT MODAL [redeem] Redeeming position for vault vault_1770234334911_7842bb57, condition 0x4799739a..., indexSets: [1], negRisk: undefined, negRiskMarketId: undefined... [redeemPosition] Starting preflight for condition 0x4799739a... ✓ Safe is deployed collateral candidates: `[ "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" ]` conditionId: `0x4799739a5074388ab70358c868a53df90136ff41b2bd31343b999dc4fea41305` indexSets: [1] Preflight (collateral=0x2791Bca1...): resolved=true, success=false, totalBalance=0 Preflight (collateral=0x3c499c54...): resolved=true, success=false, totalBalance=0 Error redeeming position: ... ##### EXECUTION FROM REGULAR POSITION EXIT MODAL on vault manager vault page [redeem] Redeeming position for vault vault_1770234334911_7842bb57, condition 0x4799739a..., indexSets: [2], negRisk: false, negRiskMarketId: undefined... [redeemPosition] Starting preflight for condition 0x4799739a... ✓ Safe is deployed collateral candidates: `[ "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" ]` conditionId: `0x4799739a5074388ab70358c868a53df90136ff41b2bd31343b999dc4fea41305` indexSets: [2] Preflight (collateral=0x2791Bca1...): resolved=true, success=true, totalBalance=1108910 ✓ Preflight passed with collateral=0x2791Bca1..., executing redemption... Executing redemption for condition 0x4799739a... Index sets: [2] Created Safe Transaction Request: ### Redeemed Positions from Activity over Closed Positions #### Prefer this: the reliable approach is to use the /activity endpoint with type=REDEEM for the same user + market. the activity system explicitly tracks REDEEM. if a REDEEM record exists it was a claim, if only TRADE records with side=SELL exist it was a sell. some positions might have both (partial sell before resolution, rest redeemed after) re: the 0.5 case - yes, you need to account for it. dispute resolution via UMA can resolve a market as "Unknown/50-50" with payouts [1,1], stored as outcome_prices = ["0.5","0.5"]. claims on voided markets would show curPrice = 0.5 and be misclassified as sells under this heuristic #### To this: Closed Positions include positions that were not claimed, but sold. - how do the numbers work? totalBought, realizedPnl, curPrice - at what moment? - does the timestamp refer to the moment of exit? - does it affect us in correctly identifying redeemed vs simply exited positions - `clarifyPositionStates` should only count as redemptions: closed-positions with timestamp after the roll and with current price !== 1; the check with associated market and closed state is not reliable, since it can change in time from not closed to closed; need to ensure the calculations of proceeds are still correct then - Account for update on how to identify claims vs trades - Liquidation History after the roll (historical) - incorrect cash amount used the curPrice heuristic is partially reliable but has an important caveat: it's the current live market price, not a snapshot from when the position was closed. it's read from outcome_prices on the market at query time, so it tells you the current state of the market, not how the position was actually closed