Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ deployments/*.json
!deployments/allowlists/
!deployments/allowlists/*.json

# Snapshot signed payloads (local only)
space-message*.json

# Logs
*.log
npm-debug.log*
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ KEYSTORE_PATH=keystores/deployer-0x<address>.json

## Deployment

### Deployment outputs

- **Source of truth**: `config/chains.json`
- **Local backups**: `deployments/*.json` (gitignored)

### Full Chain Deployment

Deploys all modules + aggregator:
Expand Down Expand Up @@ -97,6 +102,33 @@ pnpm exec hardhat verify --network <chain> <ADDRESS> <ARGS>

Get the aggregator address from `config/chains.json` → `chains.<chain>.deployed.aggregator`.

## Debugging Snapshot Settings

To confirm what Snapshot Hub currently has stored for a space:

```bash
pnpm exec tsx scripts/check-snapshot-space.ts
SPACE=quickvote.eth pnpm exec tsx scripts/check-snapshot-space.ts
```

## Publishing Snapshot Settings (Safe)

When using Safe multi-sig, Snapshot space updates are signed as a Safe “message”, but the signed payload still needs to be broadcast to Snapshot Hub.

1) Save the typed-data JSON you signed (the object with `domain`, `types`, `primaryType`, `message`) to a file, e.g. `space-message.json`.

2) Publish it using the Safe prepared signature:

```bash
pnpm exec tsx scripts/publish-snapshot-settings.ts --file ./space-message.json --sig 0x<preparedSignature>
```

3) Verify it’s live:

```bash
pnpm exec tsx scripts/check-snapshot-space.ts
```

## Testing

```bash
Expand Down
77 changes: 77 additions & 0 deletions scripts/check-snapshot-space.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Check Snapshot space settings (strategies) directly from Snapshot Hub.
*
* Usage:
* pnpm exec tsx scripts/check-snapshot-space.ts
* SPACE=quickvote.eth pnpm exec tsx scripts/check-snapshot-space.ts
*
* Notes:
* - Snapshot settings updates are off-chain. A Safe "message" being signed does NOT
* guarantee the settings were published to Snapshot Hub.
*/

type Strategy = { name: string; network: string; params: Record<string, unknown> };

const SPACE = process.env.SPACE || "quickvote.eth";
const HUB_URL = process.env.SNAPSHOT_HUB_URL || "https://hub.snapshot.org/graphql";

const query = `
query Space($id: String!) {
space(id: $id) {
id
name
network
strategies {
name
network
params
}
}
}
`;

function shortAddr(v: unknown): string {
if (typeof v !== "string") return String(v);
if (!v.startsWith("0x") || v.length < 10) return v;
return `${v.slice(0, 6)}…${v.slice(-4)}`;
}

async function main() {
const res = await fetch(HUB_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ query, variables: { id: SPACE } }),
});

if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Snapshot Hub request failed (${res.status}): ${text.slice(0, 300)}`);
}

const json = (await res.json()) as any;
if (json.errors?.length) {
throw new Error(`Snapshot Hub GraphQL error: ${JSON.stringify(json.errors, null, 2)}`);
}

const space = json.data?.space as
| { id: string; name: string; network: string; strategies: Strategy[] }
| null;
if (!space) throw new Error(`Space not found: ${SPACE}`);

console.log(`Space: ${space.name} (${space.id})`);
console.log(`Default network: ${space.network}`);
console.log(`Strategies (${space.strategies.length}):`);
for (const [i, s] of space.strategies.entries()) {
const addr = (s.params as any)?.address;
console.log(` ${i + 1}. ${s.name} (network=${s.network}) address=${addr ? shortAddr(addr) : "n/a"}`);
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});




128 changes: 128 additions & 0 deletions scripts/publish-snapshot-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Publish a signed Snapshot Space settings message to Snapshot Hub.
*
* This is the missing step when using Safe "Messages": collecting signatures in Safe does NOT
* automatically broadcast the signed payload to Snapshot Hub.
*
* Usage:
* pnpm exec tsx scripts/publish-snapshot-settings.ts --file ./space-message.json --sig 0x...
*
* Optional:
* --hub https://hub.snapshot.org
* --space quickvote.eth
*
* The JSON file must contain the EIP-712 typed data:
* { domain, primaryType, types, message }
*
* Notes:
* - Do NOT re-serialize `message.settings` yourself. It must match exactly what was signed.
* - For Safe, `--sig` should be the Safe "prepared signature" (concatenated signatures).
*/

type TypedData = {
domain: { name: string; version: string };
primaryType: string;
types: Record<string, Array<{ name: string; type: string }>>;
message: { from: string; space: string; timestamp: string | number; settings: string };
};

import { getAddress } from "viem";

function getArg(flag: string): string | undefined {
const i = process.argv.indexOf(flag);
if (i === -1) return undefined;
return process.argv[i + 1];
}

async function postJson(url: string, body: unknown) {
const res = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});

const text = await res.text().catch(() => "");
let json: any = undefined;
try {
json = text ? JSON.parse(text) : undefined;
} catch {
// ignore
}

if (!res.ok) {
throw new Error(`Hub request failed (${res.status}): ${text.slice(0, 500)}`);
}

return json ?? text;
}

async function main() {
const file = getArg("--file") || process.env.SNAPSHOT_TYPED_DATA_FILE;
const sig = getArg("--sig") || process.env.SNAPSHOT_SIG;
const hub = (getArg("--hub") || process.env.SNAPSHOT_HUB || "https://hub.snapshot.org").replace(/\/+$/, "");
const overrideSpace = getArg("--space") || process.env.SNAPSHOT_SPACE;

if (!file) {
throw new Error("Missing --file (or SNAPSHOT_TYPED_DATA_FILE).");
}
if (!sig || !sig.startsWith("0x")) {
throw new Error("Missing --sig (or SNAPSHOT_SIG). Expected 0x-prefixed hex string.");
}

const fs = await import("node:fs");
const raw = fs.readFileSync(file, "utf8");
const typed = JSON.parse(raw) as TypedData;

// Normalize without changing semantics:
// - timestamp must be a number for some encoders; keep value identical.
const timestamp =
typeof typed.message.timestamp === "string" ? Number(typed.message.timestamp) : typed.message.timestamp;
if (!Number.isFinite(timestamp)) throw new Error("Invalid message.timestamp");

const message = {
...typed.message,
timestamp,
space: overrideSpace ?? typed.message.space,
};

// Snapshot Hub expects EIP-55 checksummed addresses in the envelope.
// Also normalize message.from to checksum to avoid mismatches.
const from = getAddress(message.from);
message.from = from;

const data = {
domain: typed.domain,
types: typed.types,
primaryType: typed.primaryType,
message,
};

// Snapshot Hub expects:
// { address, sig, data }
// Where `address` is the signer (Safe address for EIP-1271 verification).
const payload = {
address: from,
sig,
data,
};

console.log("Publishing to Snapshot Hub…");
console.log(" hub:", hub);
console.log(" space:", message.space);
console.log(" from:", message.from);
console.log(" api:", `${hub}/api/msg`);

const result = await postJson(`${hub}/api/msg`, payload);
console.log("\n✅ Hub response:");
console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));

console.log("\nNext: verify with:");
console.log(" pnpm exec tsx scripts/check-snapshot-space.ts");
}

main().catch((e) => {
console.error(e);
process.exit(1);
});