# ObelusDAO epoch-hop architecture

This is the post-HALO model. Every Pyramid continues to create and control its
own Resolve token. There is no unified Resolve token and no conversion path.

## Contracts

1. **Pyramid / `$PyramidFactory`** (`pyramid.sol`) creates independent
   Pyramids. Each Pyramid deploys a distinct Resolve supply. This existing base
   remains unchanged.
2. **PyramidRouter** records the active Pyramid and active Resolve token,
   challenges the highest Pyramid, files the system-level hop request, and
   activates epochs. It never takes custody of Resolve.
3. **RoundTable** is one governance contract holding a table for every
   Pyramid, each backed only with that Pyramid's own Resolve. Every table can
   campaign, back and seat watchers at any time; only the active Pyramid's
   table answers ordinary tickets.
4. **OracleRequestBook** owns one monotonically increasing ticket namespace.
   Each ticket is bound to the active epoch and to the table that answers it:
   the active table, or for a hop request the target Pyramid's table.
5. **InertiaOracleAdapter** gives `InertiaMarket` a stable logical request ID.
   It maps that ID to the current epoch-bound request-book ticket and replaces
   only the physical ticket when a hop makes it stale.

## Hop invariant

An activation is the tuple:

```text
(epoch, pyramid address, Resolve token address, activation block)
```

Every approved hop increments `epoch`, even when it revives a Pyramid used in
an earlier epoch. A revived Pyramid reuses its own Resolve supply and its own
table, as staffed at that moment, and gets a fresh ticket epoch.

## Hops

A hop is a system-level request. When a Pyramid becomes the highest, anyone
calls `requestPyramidHop`; the ticket is answered by the target Pyramid's own
table, which must have at least one seated watcher. The active table has no
vote. Its watchers can be punished for skipping it like any ticket. When the
vote ends, the target must still have a higher net value than the active
Pyramid, so a lead that lasted one transaction cannot hop.

When the hop executes, the target's table becomes the active table with its
seats and stakes intact, and its watchers vote on new tickets immediately. The
old table's seats become *retiring*: they cannot be bumped, and once the old
table's punishment window has passed anyone dissolves them into the money bomb.
The old table is then an ordinary standby table that anyone may restaff.

The activation transaction updates the router, Round Table, and request book
atomically. If any module rejects the new tuple, the whole hop reverts.

## Tickets, voting and punishment

Tickets do not follow the table into a new epoch. A ticket stores:

- the active epoch at filing;
- the table (Pyramid) that answers it and that Pyramid's Resolve token;
- commit and reveal deadlines; and
- its fee.

Only watchers seated continuously since the ticket was filed may commit or
reveal; a watcher seated later, or bumped before revealing, cannot vote on it.
There is no quorum: after the reveal phase every ticket finalizes on the
heaviest revealed option. If nobody reveals, or an objective vote ties, the
result is 0 and no one is blamed for their vote.

Turnout is enforced by punishment, as in the original DAO. Anyone may call:

- `punishAbsent(ticketId, watcher)` for an eligible watcher that did not
  commit by the end of the commit phase or did not reveal by the end of the
  reveal phase; and
- `punishWrongVote(ticketId, watcher)` for an eligible watcher whose revealed
  vote differs from a finalized, vote-decided objective result.

Punishment unseats the watcher and voids its backing; see below for where
its stake goes.

The core oracle never refunds. The fee pays the revealing watchers by backing,
or every seated watcher by backing if nobody revealed. With no seated watcher
it carries into the next payout.

## Backing, seats and the money bomb

Backing waits in the Round Table, unstaked, until its candidate wins a seat.
Seating stakes the candidate's whole backing into the table's Pyramid as one
stake. Backing cannot be added to or withdrawn from a seated watcher, so its
weight is fixed for every ticket it votes on, and every backer of a seat joins
its stake at the same moment; a backer's share is its share of the backing.
Candidates campaign with a reward share and a suggested table size
(`registerCandidate(metadata, rewardShareBps, suggestedSeats)`), both locked
while they hold backing. All of a candidate's live backing votes for its
suggested size; the most-backed size wins. Growth applies as backing arrives;
anyone may apply a shrink with `updateTableSize()`.

Leaving the table involves no choice:

- **Bumped** by a better-backed candidate or a shrinking table (no
  wrongdoing). The stake is pulled out of the Pyramid, its dividends are
  forfeited to the Pyramid's other stakers, and the Resolve returns as waiting
  backing. Backers may withdraw it, back someone else, or leave it for the
  candidate to win a seat again.
- **Retired** by a hop. Once the old table's punishment window (its last
  ticket's reveal phase plus one more reveal-length) has passed, anyone calls
  `dissolve`: every earning is withdrawn and all of the seat's Resolve is
  burned; the payout, together with the seat's share of the table's bomb pot,
  pays the watcher its reward share and backers `settle` for the rest by share
  (the money bomb).
- **Punished**. The backing is voided and the stake is dissolved at once: its
  earnings are withdrawn, its Resolve is burned, and the money joins the
  table's `bombPot`. When the table retires, each dissolved seat also takes a
  share of the pot by its backing. Backers of the punished seat `settle` to clear their
  position and receive nothing. (The Pyramid cannot withdraw a stake with no
  earnings; such a stake stays locked.)

Payouts are ETH through `ethCredits` and `withdrawEth`.

## Tickets at a hop

Once a hop increments the active epoch, every other open ticket from the old
epoch is *stale*. It cannot accept commits, reveals, or finalization. Anyone may
call `dropStale(ticketId)`, which cancels it; the fee pays the seated watchers
of the table that was answering it and is not refunded. Any refund to an end user belongs to the
requesting application. This is lazy invalidation: hops never loop over all
outstanding tickets.

For Inertia markets, the adapter restores progress through
`refileStaleOrExpiredRequest(logicalTicketId)`. It drops the stale physical
ticket and files the same payload against the new epoch; the caller pays the
full new fee. The logical ticket ID delivered to the market does not change.
The market's shares, pool, order book, and original collateral token are not
migrated or reset by the hop.

## Backing at a hop

Old backing is not converted. Unstaked backing stays withdrawable in its
original epoch and Resolve token; seated terms are dissolved as described
above. Only the active epoch's token
can create new backing, so an old Resolve supply cannot influence the new Round
Table unless its Pyramid is deliberately revived in a later hop.

## Deployment order

1. Deploy `$PyramidFactory`, call `createPyramid(DEFAULT_SLOPE)` in a separate
   transaction, and select the returned initial Pyramid. The split is required
   by Ethereum Classic's current 8M block gas limit.
2. Deploy `PyramidRouter(factory, initialPyramid)`.
3. Deploy `RoundTable(router, seatLimit)`.
4. Deploy `OracleRequestBook(router, roundTable, commitBlocks, revealBlocks,
   baseFeePerWatcher)`.
5. Call `router.configureModules(roundTable, requestBook)` once.
6. Deploy `InertiaOracleAdapter(requestBook)`.
7. Deploy `InertiaMarket(collateralToken, adapter, admin, discountAdmin)`.

The one-time bootstrap authority is burned by step 5. It cannot force hops.

Timing, seat count, and fee are constructor inputs rather than hidden
economic assumptions. They should be chosen explicitly for a deployment or
experiment.
