Skip to content

Commit feea7ce

Browse files
authored
Merge pull request #1350 from graphprotocol/tmigone/split-ird
refactor(deployment): split deployment of DIPs from REO + issuance
2 parents 1f975c9 + bc84c86 commit feea7ce

4 files changed

Lines changed: 137 additions & 20 deletions

File tree

packages/deployment/config/arbitrumOne.json5

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010
// sum to issuancePerBlock, which must equal RM's on-chain issuance rate — the
1111
// scripts error on mismatch and never set the rate from config.
1212
//
13-
// issuancePerBlock is RM's live on-chain rate (120.73 GRT/block). RAM keeps
14-
// its intended 6; RM self-mints the remainder (120.73 - 6 = 114.73).
13+
// DIPs-dormant rollout: RecurringAgreementManager is intentionally left out of
14+
// the table so it receives 0 issuance — the protocol-funded indexing-agreement
15+
// path is off at launch. RM self-mints the full on-chain rate (120.73 GRT/block).
16+
// `issuance-connect` already puts RM at 100%, so `issuance-allocate` is a no-op.
17+
//
18+
// DIP activation (later) restores the split here — e.g. RM 114.73 + RAM 6 — and
19+
// re-runs issuance-allocate, alongside the guardian unpausing RecurringCollector
20+
// (the payer-funded path; managed out-of-band — see Gip0088Runbook.md).
1521
issuancePerBlock: '120.73',
1622
allocations: {
17-
RewardsManager: { selfGrtPerBlock: '114.73' },
18-
RecurringAgreementManager: { allocatorGrtPerBlock: '6' },
23+
RewardsManager: { selfGrtPerBlock: '120.73' },
1924
},
2025
},
2126
}

packages/deployment/config/arbitrumSepolia.json5

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@
99
// Explicit issuance allocation table, by target contract name. The rates must
1010
// sum to issuancePerBlock, which must equal RM's on-chain issuance rate — the
1111
// scripts error on mismatch and never set the rate from config.
12+
//
13+
// DIPs-dormant target: RecurringAgreementManager is left out of the table so it
14+
// receives 0 issuance (protocol-funded indexing-agreement path off). RM self-mints
15+
// the full rate (6.0365 GRT/block). The chain currently still has the active split
16+
// (RM 5.5365 + RAM 0.5); re-run `GIP-0088:issuance-allocate` to converge to this
17+
// table (it emits setTargetAllocation(RAM,0,0) then setTargetAllocation(RM,0,6.0365),
18+
// decrease-first), and have the guardian pause RecurringCollector out-of-band.
1219
issuancePerBlock: '6.0365',
1320
allocations: {
14-
RewardsManager: { selfGrtPerBlock: '5.5365' },
15-
RecurringAgreementManager: { allocatorGrtPerBlock: '0.5' },
21+
RewardsManager: { selfGrtPerBlock: '6.0365' },
22+
RecurringAgreementManager: { allocatorGrtPerBlock: '0' },
23+
},
1624
},
1725
},
1826
}

packages/deployment/docs/Gip0088Runbook.md

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ naming is the source of truth for ordering.
1818

1919
## Stage & Gate index
2020

21-
| Phase | Stages | Gates |
22-
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
23-
| Entry — Readiness || [G1](#gate-g1) |
24-
| A — Contract deployment | [S1](#stage-s1) [S2](#stage-s2) [S3](#stage-s3) | [G2](#gate-g2) [G3](#gate-g3) [G4](#gate-g4) |
25-
| B — Proxy upgrade | [S4](#stage-s4) [S5](#stage-s5) | [G5](#gate-g5) [G6](#gate-g6) |
26-
| C — Activation | [S6](#stage-s6) [S7](#stage-s7) [S8](#stage-s8) [S9](#stage-s9) [S10](#stage-s10) [S11](#stage-s11) | [G7](#gate-g7) [G8](#gate-g8) [G9](#gate-g9) [G10](#gate-g10) [G11](#gate-g11) [G12](#gate-g12) |
27-
| D — Off-chain & close-out (testnet / mainnet only) | [S12](#stage-s12) [S13](#stage-s13) [S14](#stage-s14) [S15](#stage-s15) | [G13](#gate-g13) [G14](#gate-g14) [G15](#gate-g15) [G16](#gate-g16) |
21+
| Phase | Stages | Gates |
22+
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
23+
| Entry — Readiness | | [G1](#gate-g1) |
24+
| A — Contract deployment | [S1](#stage-s1) [S2](#stage-s2) [S3](#stage-s3) | [G2](#gate-g2) [G3](#gate-g3) [G4](#gate-g4) |
25+
| B — Proxy upgrade | [S4](#stage-s4) [S5](#stage-s5) | [G5](#gate-g5) [G6](#gate-g6) |
26+
| C — Activation | [S5a](#stage-s5a) [S6](#stage-s6) [S7](#stage-s7) [S8](#stage-s8) [S9](#stage-s9) [S10](#stage-s10) [S11](#stage-s11) | [G6a](#gate-g6a) [G7](#gate-g7) [G8](#gate-g8) [G9](#gate-g9) [G10](#gate-g10) [G11](#gate-g11) [G12](#gate-g12) |
27+
| D — Off-chain & close-out (testnet / mainnet only) | [S12](#stage-s12) [S13](#stage-s13) [S14](#stage-s14) [S15](#stage-s15) | [G13](#gate-g13) [G14](#gate-g14) [G15](#gate-g15) [G16](#gate-g16) |
2828

2929
## Environments
3030

@@ -283,10 +283,10 @@ run record.
283283

284284
### Gate G6 — upgrade-complete
285285

286-
| Field | Value |
287-
| ---------------- | --------------------- |
288-
| Postcondition of | [Stage S5](#stage-s5) |
289-
| Precondition of | [Stage S6](#stage-s6) |
286+
| Field | Value |
287+
| ---------------- | ----------------------- |
288+
| Postcondition of | [Stage S5](#stage-s5) |
289+
| Precondition of | [Stage S5a](#stage-s5a) |
290290

291291
**Check.**
292292

@@ -316,6 +316,58 @@ Eligibility-integrate ([S6](#stage-s6)/[S7](#stage-s7)) is a no-op only when
316316
`RM.providerEligibilityOracle` already matches the configured oracle; if it
317317
differs, the stage re-points it (config is the source of truth).
318318

319+
**DIPs-dormant rollout.** This environment ships with on-chain indexing
320+
agreements (DIPs) **off**, via two independent levers — neither turned on by the
321+
activation goals:
322+
323+
- **Protocol-funded path**`IssuanceAllocator.allocations` in
324+
`config/<network>.json5` omits `RecurringAgreementManager`, so RAM receives 0
325+
issuance. `issuance-connect` puts RM at 100%, so `issuance-allocate`
326+
([S10](#stage-s10)/[S11](#stage-s11)) is a no-op — run it only to confirm.
327+
- **Payer-funded path**`RecurringCollector` is paused ([S5a](#stage-s5a)).
328+
This is a **pause-guardian** action managed out-of-band, not by the deploy
329+
package; `09_end` does **not** verify it, so [G6a](#gate-g6a) is the gate that
330+
does.
331+
332+
Turning DIPs on later is the inverse of both levers — see
333+
[Activating DIPs later](#activating-dips-later).
334+
335+
<a id="stage-s5a"></a>
336+
337+
### Stage S5a — Pause RecurringCollector (DIPs dormant)
338+
339+
| Field | Value |
340+
| -------------- | ------------------------------------------------ |
341+
| Phase | C — Activation |
342+
| Actor | Pause guardian (out-of-band; see below) |
343+
| Entry gate | [G6 upgrade-complete](#gate-g6) |
344+
| Exit gate | [G6a dips-dormant](#gate-g6a) |
345+
| Parallelizable | No |
346+
| Reference | `RecurringCollector.pause()` — onlyPauseGuardian |
347+
348+
**What this does.** Pauses `RecurringCollector` so no indexing agreement can be
349+
accepted, collected, updated, or cancelled (`accept`/`collect`/`update`/`cancel`
350+
are all `whenNotPaused`). This closes the payer-funded DIP path; the
351+
protocol-funded path is already off via config (RAM unallocated).
352+
353+
**Why out-of-band.** `RC.pause()` is `onlyPauseGuardian`, and the pause guardian
354+
(`Controller.pauseGuardian()`) is a distinct actor from the protocol governor by
355+
design — a separate Safe on mainnet (`0xB0aD…3aAE`), a separate EOA on testnet
356+
(`0xa044…20D7`). The deploy package only emits governor batches, so it neither
357+
sets nor clears pause state; the guardian performs this directly.
358+
359+
**When.** As soon as [G6](#gate-g6) lands — the guardian role is granted in the
360+
[S4](#stage-s4) upgrade batch, so the guardian can act the moment the upgrade
361+
executes. RC is unpaused by default (its initializer leaves it live), so pause
362+
promptly to minimise the window.
363+
364+
**Steps.**
365+
366+
1. Guardian executes `RecurringCollector.pause()`:
367+
- Mainnet: propose & execute on the pause-guardian Safe (`0xB0aD…3aAE`).
368+
- Testnet: `cast send <RC> "pause()" --from <guardian>` with the guardian EOA
369+
key (`0xa044…20D7`); the governor key cannot do this.
370+
319371
<a id="stage-s6"></a>
320372

321373
### Stage S6 — Generate the eligibility-integrate batch
@@ -324,7 +376,7 @@ differs, the stage re-points it (config is the source of truth).
324376
| -------------- | ------------------------------------------------------------ |
325377
| Phase | C — Activation |
326378
| Actor | Deployer (EOA) |
327-
| Entry gate | [G6 upgrade-complete](#gate-g6) |
379+
| Entry gate | [G6a dips-dormant](#gate-g6a) |
328380
| Exit gate | [G7 eligibility-batch-reviewed](#gate-g7) |
329381
| Parallelizable | No |
330382
| Reference | [Gip0088.md — Activation goals](Gip0088.md#activation-goals) |
@@ -418,6 +470,11 @@ rate (no rebalancing). **Exits** unless the config `issuancePerBlock` equals RM'
418470
on-chain rate and the per-target rates sum to it. Skips when no allocations are
419471
configured.
420472

473+
In the **DIPs-dormant** config the only target is RM, already at 100% from
474+
[S8](#stage-s8) `issuance-connect`, so this stage emits **no transactions**
475+
run it to confirm the table matches on-chain. (When DIPs are later activated, the
476+
RAM allocation is added back to config and this stage does the real work.)
477+
421478
**Steps.**
422479

423480
1. `pnpm hardhat deploy --tags GIP-0088:issuance-allocate --network <network>`
@@ -438,7 +495,29 @@ configured.
438495

439496
1. `pnpm hardhat deploy:execute-governance --network <network>` (fork/testnet) or
440497
council Safe execution (mainnet).
441-
<a id="gate-g7"></a>
498+
<a id="gate-g6a"></a>
499+
500+
### Gate G6a — dips-dormant
501+
502+
| Field | Value |
503+
| ---------------- | ----------------------- |
504+
| Postcondition of | [Stage S5a](#stage-s5a) |
505+
| Precondition of | [Stage S6](#stage-s6) |
506+
507+
**Check.** `cast call <RecurringCollector> "paused()(bool)" --rpc-url <rpc>` (or
508+
the equivalent read). Confirm `true`.
509+
510+
**Pass criterion.** `RC.paused() == true`. With RAM unallocated (config) and RC
511+
paused, neither DIP funding path can create or collect an agreement.
512+
513+
**Note.** `09_end` (the [G12](#gate-g12) assertion) does **not** check pause
514+
state — it would report GIP-0088 "complete" with RC still live. This gate is the
515+
only check that the payer-funded path is closed; do not skip it.
516+
517+
**If it fails.** RC is still live — re-run [S5a](#stage-s5a) (have the guardian
518+
execute `pause()`). Until this gate passes the rollout is not dormant.
519+
520+
<a id="gate-g7"></a>
442521

443522
### Gate G7 — eligibility-batch-reviewed
444523

@@ -545,7 +624,10 @@ when RM self-minting covers the slack).
545624

546625
**Pass criterion.** Exit 0 — upgrade, issuance-connect and eligibility-integrate
547626
all verified; the configured REO is the active oracle; and both
548-
`revertOnIneligible` and the RAM allocation rates match config.
627+
`revertOnIneligible` and the allocation rates match config (RAM unallocated in
628+
the dormant config). Note: `09_end` does **not** check `RC.paused()` — dormancy
629+
of the payer-funded path is attested by [G6a](#gate-g6a), not here, so both gates
630+
must pass for the rollout to be dormant.
549631

550632
**If it fails.** Re-run the stage that owns the unmet goal as named in the
551633
output.
@@ -734,6 +816,25 @@ prior stage. Abort is clean before [G4](#gate-g4); after, recovery needs a
734816
follow-up governance batch — see
735817
[GovernanceWorkflow.md](GovernanceWorkflow.md).
736818

819+
## Activating DIPs later
820+
821+
DIPs ship dormant (see [Phase C](#phase-c--activation)). Turning them on is the
822+
inverse of the two dormancy levers, and is a **separate, later** change — not
823+
part of this rollout:
824+
825+
1. **Restore the issuance split** — in `config/<network>.json5`, add the
826+
`RecurringAgreementManager` allocation back to `IssuanceAllocator.allocations`
827+
(e.g. mainnet `RewardsManager: 114.73` + `RecurringAgreementManager: 6`), then
828+
run `GIP-0088:issuance-allocate` and execute the governor batch. `ia:status`
829+
should then show RAM funded. The decrease-first ordering means RM drops and RAM
830+
rises within one batch.
831+
2. **Unpause the collector** — the pause guardian executes
832+
`RecurringCollector.unpause()` (mainnet: guardian Safe `0xB0aD…3aAE`; testnet:
833+
guardian EOA `0xa044…20D7`). The governor cannot do this.
834+
835+
Each lever is independently reversible: re-apply the RM-only config + re-run
836+
`issuance-allocate` to de-fund RAM, and have the guardian `RC.pause()` again.
837+
737838
## See also
738839

739840
- [Gip0088.md](Gip0088.md) — reference guide: scripts, tags, preconditions, model

packages/deployment/docs/gip0088/runs/Template.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ the reason in the Waivers section.
5050

5151
### Phase C — Activation
5252

53+
- [ ] **S5a** Pause RecurringCollector (DIPs dormant) — guardian / date / by:
54+
tx hash:
55+
- [ ] **G6a** dips-dormant (`RC.paused() == true`) — PASS date / by:
5356
- [ ] **S6** Generate the eligibility-integrate batch — date / by:
5457
batch file:
5558
- [ ] **G7** eligibility-batch-reviewed — PASS date / reviewer:

0 commit comments

Comments
 (0)