Skip to content

Commit 7054f73

Browse files
committed
feat(deploy): support bypassing on chain assumptions for sequenced
execution Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
1 parent cafa759 commit 7054f73

5 files changed

Lines changed: 171 additions & 17 deletions

File tree

packages/deployment/deploy/gip/0088/eligibility_integrate.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@graphprotocol/deployment/lib/contract-registry.js'
1010
import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js'
1111
import { getResolvedSettingsForEnv } from '@graphprotocol/deployment/lib/deployment-config.js'
12-
import { ComponentTags, GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js'
12+
import { assumeUpgraded, ComponentTags, GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js'
1313
import { requireContracts } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js'
1414
import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js'
1515
import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js'
@@ -43,6 +43,23 @@ async function integrateOracle(
4343
await syncComponentsFromRegistry(env, [reoEntry, targetEntry])
4444
const [reo, target] = requireContracts(env, [reoEntry, targetEntry])
4545

46+
const { governor, canSign } = await canSignAsGovernor(env)
47+
48+
// Sequenced-bundle generation: the target proxy isn't upgraded yet, so the
49+
// oracle getter would revert. Skip the probe/idempotency read and emit the
50+
// set-oracle TX unconditionally. The resulting bundle is sequenced-only —
51+
// execute it after the upgrade bundle (nonce order enforces this).
52+
if (assumeUpgraded()) {
53+
await applyConfiguration(env, client, [createRMIntegrationCondition(reo.address)], {
54+
contractName: `${target.name}-REO`,
55+
contractAddress: target.address,
56+
canExecuteDirectly: canSign,
57+
executor: governor,
58+
assumeUndone: true,
59+
})
60+
return
61+
}
62+
4663
// Skip only if the target isn't upgraded yet (no oracle getter). Once it
4764
// supports the getter, config is the source of truth: applyConfiguration is
4865
// idempotent — it re-points the oracle to the configured REO when the current
@@ -59,8 +76,6 @@ async function integrateOracle(
5976
return
6077
}
6178

62-
const { governor, canSign } = await canSignAsGovernor(env)
63-
6479
await applyConfiguration(env, client, [createRMIntegrationCondition(reo.address)], {
6580
contractName: `${target.name}-REO`,
6681
contractAddress: target.address,

packages/deployment/deploy/gip/0088/issuance_connect.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
GRAPH_TOKEN_ABI,
33
ISSUANCE_ALLOCATOR_ABI,
44
ISSUANCE_TARGET_ABI,
5+
REWARDS_MANAGER_DEPRECATED_ABI,
56
SET_TARGET_ALLOCATION_ABI,
67
} from '@graphprotocol/deployment/lib/abis.js'
78
import { getTargetChainIdFromEnv } from '@graphprotocol/deployment/lib/address-book-utils.js'
@@ -11,7 +12,7 @@ import {
1112
} from '@graphprotocol/deployment/lib/contract-checks.js'
1213
import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js'
1314
import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js'
14-
import { ComponentTags, GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js'
15+
import { assumeUpgraded, ComponentTags, GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js'
1516
import {
1617
createGovernanceTxBuilder,
1718
executeTxBatchDirect,
@@ -68,9 +69,18 @@ export default createActionModule(
6869
// Create viem client for direct contract calls
6970
const client = graph.getPublicClient(env) as PublicClient
7071

71-
// Check if RewardsManager supports IIssuanceTarget (has been upgraded)
72-
// Throws error if not upgraded
73-
await requireRewardsManagerUpgraded(client, rmAddress, env)
72+
const sequenced = assumeUpgraded()
73+
74+
// Check if RewardsManager supports IIssuanceTarget (has been upgraded).
75+
// Throws if not upgraded — skipped under sequenced generation, where this
76+
// bundle is built to execute right after the upgrade bundle (nonce order).
77+
if (!sequenced) {
78+
await requireRewardsManagerUpgraded(client, rmAddress, env)
79+
} else {
80+
env.showMessage(
81+
'\n⚠ Sequenced generation: RM upgrade assumed — this bundle is SEQUENCED-ONLY and valid only AFTER the upgrade bundle executes (nonce order).\n',
82+
)
83+
}
7484

7585
const targetChainId = await getTargetChainIdFromEnv(env)
7686

@@ -86,7 +96,35 @@ export default createActionModule(
8696
// Sub-flags drive both the per-line status display and which TXs the build-batch needs.
8797
env.showMessage('📋 Checking current activation state...\n')
8898

89-
const connect = await checkIssuanceConnectComplete(client, iaAddress, rmAddress, gtAddress)
99+
// Sequenced generation: RM isn't upgraded yet, so RM.getIssuanceAllocator (the
100+
// iaIntegrated read inside checkIssuanceConnectComplete) would revert. Assume the
101+
// RM-side wiring is undone and emit it. The rate invariant below is still enforced
102+
// — both rates are readable on the un-upgraded RM. IA-side reads (default target)
103+
// stay live in the TX-build section downstream.
104+
const connect = sequenced
105+
? {
106+
complete: false,
107+
iaIntegrated: false,
108+
iaMinter: false,
109+
rmAllocationShape: false,
110+
fullyAllocated: false,
111+
iaRate: (await client.readContract({
112+
address: iaAddress as `0x${string}`,
113+
abi: ISSUANCE_ALLOCATOR_ABI,
114+
functionName: 'getIssuancePerBlock',
115+
})) as bigint,
116+
rmRate: (await client.readContract({
117+
address: rmAddress as `0x${string}`,
118+
abi: REWARDS_MANAGER_DEPRECATED_ABI,
119+
functionName: 'issuancePerBlock',
120+
})) as bigint,
121+
get ratesAligned(): boolean {
122+
return this.iaRate === this.rmRate
123+
},
124+
currentIssuanceAllocator: '(unknown — RM not upgraded)',
125+
rmAllocation: { selfMintingRate: 0n, allocatorMintingRate: 0n },
126+
}
127+
: await checkIssuanceConnectComplete(client, iaAddress, rmAddress, gtAddress)
90128

91129
env.showMessage(
92130
` IA integrated: ${connect.iaIntegrated ? '✓' : '✗'} (current: ${connect.currentIssuanceAllocator})`,

packages/deployment/docs/Gip0088Runbook.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,57 @@ prior stage. Abort is clean before [G4](#gate-g4); after, recovery needs a
816816
follow-up governance batch — see
817817
[GovernanceWorkflow.md](GovernanceWorkflow.md).
818818

819+
## Sequenced bundle generation (single council signing session)
820+
821+
The default flow generates each governance bundle only _after_ the previous one
822+
executes on-chain — every activation goal reads live state and gates on the RM
823+
upgrade ([S6](#stage-s6)/[S8](#stage-s8) skip or exit until RM is upgraded). On
824+
mainnet, where the council signs M-of-N over days, that forces one signing round
825+
per stage. To hand the council **every** GIP-0088 bundle at once, set
826+
`GIP_0088_ASSUME_UPGRADED=1` when generating the activation bundles:
827+
828+
```bash
829+
# Upgrade bundle — generated normally (already carries the RM-gated config:
830+
# setDefaultReclaimAddress + setRevertOnIneligible, ordered after the RM upgrade)
831+
pnpm hardhat deploy --tags GIP-0088:upgrade,upgrade --network arbitrumOne
832+
833+
# Activation bundles — generated ahead of the upgrade executing
834+
GIP_0088_ASSUME_UPGRADED=1 pnpm hardhat deploy --tags GIP-0088:eligibility-integrate --network arbitrumOne
835+
GIP_0088_ASSUME_UPGRADED=1 pnpm hardhat deploy --tags GIP-0088:issuance-connect --network arbitrumOne
836+
```
837+
838+
With the flag, `eligibility-integrate` and `issuance-connect` skip the
839+
"is RM upgraded on-chain" guard and the post-upgrade idempotency reads (which
840+
would revert against the old implementation) and emit their full tx set.
841+
842+
**Execution — nonce order is load-bearing.** These activation bundles are
843+
**sequenced-only**: valid only when executed _after_ the upgrade bundle. Queue
844+
them on the council Safe in order:
845+
846+
| Safe nonce | Bundle |
847+
| ---------- | ----------------------------------------------- |
848+
| N | `gip-0088-upgrades.json` (upgrades + RM config) |
849+
| N+1 | `eligibility-integrate` bundle |
850+
| N+2 | `gip-0088-issuance-connect.json` |
851+
852+
The Safe executes in strict nonce order, so RM is upgraded by the time N+1/N+2
853+
run, and the council reviews + signs all three in one session. If bundle N fails,
854+
N+1/N+2 are blocked rather than executing against an un-upgraded RM.
855+
856+
- **Kept:** the `issuance-connect` rate invariant
857+
(`IA.issuancePerBlock == RM.issuancePerBlock`) is still enforced — it reads
858+
`RM.issuancePerBlock`, which exists on the un-upgraded RM.
859+
- **Dropped:** idempotency. The flag blind-emits the full set, so use it only for
860+
the initial sequenced generation — **not** for re-runs or recovery, where the
861+
default (guarded) mode reads live state and emits only the remaining work.
862+
- **`issuance-allocate`** stays a no-op in the DIPs-dormant config (RM already
863+
100% from `issuance-connect`), so it needs no bundle. On a DIPs-active config,
864+
generate it too, with the flag, as an `N+3` sequenced bundle.
865+
866+
This mode trades the staged per-goal review gates ([G7](#gate-g7)/[G9](#gate-g9))
867+
for a single up-front review of all bundles — a deliberate choice for the
868+
one-session council workflow, not the default.
869+
819870
## Activating DIPs later
820871

821872
DIPs ship dormant (see [Phase C](#phase-c--activation)). Turning them on is the

packages/deployment/lib/apply-configuration.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ export interface ApplyConfigurationOptions {
3434

3535
/** Account to execute from (if canExecuteDirectly) */
3636
executor?: string
37+
38+
/**
39+
* Skip the on-chain state check and treat every condition as un-applied,
40+
* emitting a TX for each. Used by sequenced-bundle generation
41+
* (`GIP_0088_ASSUME_UPGRADED`) where the target proxy isn't upgraded yet, so
42+
* the getter reads that drive the normal idempotency check would revert
43+
* against the old implementation. The resulting batch is sequenced-only —
44+
* valid only after the upgrade bundle executes.
45+
*/
46+
assumeUndone?: boolean
3747
}
3848

3949
/**
@@ -77,16 +87,34 @@ export async function applyConfiguration<T>(
7787
conditions: ConfigCondition<T>[],
7888
options: ApplyConfigurationOptions,
7989
): Promise<ApplyConfigurationResult<T>> {
80-
const { contractName, contractAddress, canExecuteDirectly, executor } = options
81-
82-
// 1. Check all conditions
83-
env.showMessage(`📋 Checking ${contractName} configuration...\n`)
84-
85-
const status = await checkConditions(client, contractAddress, conditions)
90+
const { contractName, contractAddress, canExecuteDirectly, executor, assumeUndone } = options
91+
92+
// 1. Check all conditions — or, in sequenced-generation mode, skip the read and
93+
// treat every condition as un-applied (the target proxy isn't upgraded yet, so
94+
// the getter reads would revert). The resulting batch is sequenced-only.
95+
let status: ConfigurationStatus<T | boolean>
96+
if (assumeUndone) {
97+
env.showMessage(
98+
`⚠ ${contractName}: sequenced generation — skipping on-chain check, emitting all configuration TXs\n`,
99+
)
100+
status = {
101+
allOk: false,
102+
conditions: conditions.map((c) => ({
103+
name: c.name,
104+
ok: false,
105+
current: false,
106+
target: false,
107+
message: ` (assumed un-applied) ${c.name}`,
108+
})),
109+
}
110+
} else {
111+
env.showMessage(`📋 Checking ${contractName} configuration...\n`)
112+
status = await checkConditions(client, contractAddress, conditions)
86113

87-
// Display results
88-
for (const result of status.conditions) {
89-
env.showMessage(` ${result.message}`)
114+
// Display results
115+
for (const result of status.conditions) {
116+
env.showMessage(` ${result.message}`)
117+
}
90118
}
91119

92120
// 2. If all OK, no-op

packages/deployment/lib/deployment-tags.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,28 @@ export const GoalTags = {
8181
GIP_0088_ISSUANCE_CLOSE_GUARD: 'GIP-0088:issuance-close-guard',
8282
} as const
8383

84+
/**
85+
* Sequenced-bundle generation flag (`GIP_0088_ASSUME_UPGRADED=1`).
86+
*
87+
* When set, the GIP-0088 activation goals (`eligibility-integrate`,
88+
* `issuance-connect`) build their governance bundles as if the RewardsManager
89+
* upgrade had already landed — skipping the "is RM upgraded on-chain" guard and
90+
* the post-upgrade idempotency reads that would otherwise revert against the
91+
* un-upgraded proxy.
92+
*
93+
* Purpose: generate the whole GIP-0088 governance sequence up front so a council
94+
* multisig can review and sign every bundle in ONE session, executing them in
95+
* nonce order (upgrade bundle first). The activation bundles produced this way
96+
* are SEQUENCED-ONLY — valid only when executed after the upgrade bundle; the
97+
* Safe's nonce ordering enforces that.
98+
*
99+
* Do NOT use for re-runs or recovery: it blind-emits the full tx set with no
100+
* idempotency. Use the default (guarded) mode there. The rate-alignment
101+
* invariant in `issuance-connect` is still enforced (it does not need the new
102+
* implementation).
103+
*/
104+
export const assumeUpgraded = (): boolean => process.env.GIP_0088_ASSUME_UPGRADED === '1'
105+
84106
/**
85107
* Special tags
86108
*/

0 commit comments

Comments
 (0)