Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a27e4a6
docs: Add design documents for the PEN migration to Base
ebma Aug 24, 2026
9cf7d89
pallets: Add the token-migration pallet
ebma Aug 24, 2026
e1f168a
runtime: Wire the token-migration pallet into Pendulum
ebma Aug 24, 2026
2aec0e8
contracts: Add the PEN token, migration vault and governance for Base
ebma Aug 24, 2026
eb8ba1b
attestor: Add the attestor daemon
ebma Aug 24, 2026
bb1966c
monitor: Add the independent invariant monitor
ebma Aug 24, 2026
9b6a7c8
releaser: Add the deferred-release service
ebma Aug 24, 2026
2fb8c1c
docs: Add operational runbooks, local test plan and review log
ebma Aug 24, 2026
3c47488
contracts: Fill in the decided deployment parameters
ebma Aug 24, 2026
a6f8100
testing: Add a local validation harness for the migration
ebma Aug 27, 2026
b3e31fe
docs: Refresh the implementation overview and settle the testnet plan
ebma Aug 27, 2026
298a54b
pallets: Replace estimated weights with benchmarked ones
ebma Aug 27, 2026
7c1b91c
attestor: Pad the gas limit when submitting an approval
ebma Aug 27, 2026
dcf5401
releaser: Declare Multicall3 and fall back to per-nonce reads
ebma Aug 27, 2026
58e48b2
testing: Add phase 1 and phase 3 to the validation harness
ebma Aug 27, 2026
1bb94df
attestor: Ignore local checkpoint files from test runs
ebma Aug 27, 2026
7027c81
docs: Point the test plan at the automated harness
ebma Aug 28, 2026
9aeb872
testing: Repair chain specs so the node can read back its own output
ebma Aug 28, 2026
e7ca41b
testing: Add a Zombienet network for relay-finality validation
ebma Aug 28, 2026
df0b9a8
testing: Add phase 4, relay-chain finality under Zombienet
ebma Aug 28, 2026
9c2add8
docs: Record relay-finality validation in the test plan
ebma Aug 28, 2026
ec4100d
testing: Add rehearsal environment with testnet guardrails
ebma Aug 28, 2026
e0eda12
testing: Extract Zombienet control into a shared module
ebma Aug 28, 2026
c1b2654
testing: Add phase 5, full-stack rehearsal on Base Sepolia
ebma Aug 28, 2026
53acc1e
docs: Document the full-stack rehearsal phase
ebma Aug 28, 2026
b4f13aa
testing: Fan rehearsal gas out from the deployer
ebma Aug 28, 2026
0a05626
testing: Share the collator discovery helper with phase 4
ebma Aug 28, 2026
844f731
attestor: Survive a lost race when the RPC has not caught up
ebma Aug 28, 2026
c7f4f0c
attestor: Actually route the race recheck through the backoff
ebma Aug 28, 2026
7037f6f
attestor: Survive transient RPC failures instead of exiting
ebma Aug 28, 2026
faf7939
testing: Ride out endpoint throttling in the rehearsal's own reads
ebma Aug 28, 2026
299601f
docs: Record the two defects the Base Sepolia rehearsal found
ebma Aug 28, 2026
8b86094
testing: Automate the phase 6 failure drills
ebma Aug 31, 2026
c0ba4f1
docs: Point the failure-drill table at the automated drills
ebma Aug 31, 2026
e975577
testing: Drill the real runtime upgrade under a live fleet
ebma Aug 31, 2026
f0f64e4
attestor: Cover bare 429 codes; record the post-drills review pass
ebma Aug 31, 2026
d952d5e
testing: Add phase 5b, the governance rehearsal on Base Sepolia
ebma Aug 31, 2026
16ee8bf
docs: Document the governance rehearsal and its manual walkthrough
ebma Aug 31, 2026
ac1e77b
docs: Add the review handover briefing for the next review round
ebma Aug 31, 2026
fc29bc8
pallet: Make the treasury destination a one-time anchor
ebma Sep 6, 2026
bb97e5b
attestor: Persist an identity-bound checkpoint atomically
ebma Sep 6, 2026
f49990c
attestor: Process at latest and trail the checkpoint at Base finality
ebma Sep 6, 2026
ce57ffd
releaser: Persist an identity-bound state file atomically
ebma Sep 6, 2026
4a21164
releaser: Drop pending entries only at the finality boundary
ebma Sep 6, 2026
9da3dd1
monitor: Persist the two-chain cursor and pending tuples atomically
ebma Sep 6, 2026
95e7e47
monitor: Reconcile every safe Base event against its finalized Pendul…
ebma Sep 6, 2026
7ed88c3
contracts: Base quorum on circulating supply with an absolute floor
ebma Sep 6, 2026
9d9e81f
contracts: Let a human canceller veto a queued proposal during the delay
ebma Sep 6, 2026
e3a14b9
contracts: Enforce cap invariants and reject a past sweep floor
ebma Sep 6, 2026
8f89a6f
testing: Drive the monitor's durable cursors and drill the new alarms
ebma Sep 6, 2026
12dc446
docs: Record review rounds 8 and 9 and realign the runbooks
ebma Sep 6, 2026
6ac055e
ci: Run the Foundry and daemon suites
ebma Sep 6, 2026
6622d94
attestor: Treat a pre-vault finality boundary as not yet durable
ebma Sep 6, 2026
1622aea
testing: Tee daemon logs; require an Anvil whose safe tag tracks latest
ebma Sep 6, 2026
224ff6d
releaser: Buffer the release() gas limit
ebma Sep 6, 2026
67702bd
testing: Pass explicit gas in the harness; keep the drill's caps equal
ebma Sep 6, 2026
e0a36b9
docs: Record the phase 1 and phase 3 results on the round-9 revision
ebma Sep 6, 2026
c1a2fdd
runtime: Bump the Pendulum spec version to 26
ebma Sep 8, 2026
7ee6fa5
pallet: Accept the dev-machine benchmark weights as production weights
ebma Sep 8, 2026
49f690b
docs: Record phase 2 and the RB-5 drill on the spec-26 runtime
ebma Sep 8, 2026
dce3028
runtime: Keep the Pendulum spec version at 25 on this branch
ebma Sep 8, 2026
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
65 changes: 65 additions & 0 deletions .github/workflows/migration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Migration Tests

# The PEN -> Base migration stack lives outside the Rust workspace: the
# Foundry contracts and the three TypeScript daemons. `test-code.yml` covers
# the pallet through `cargo test`; this job covers everything else, so a
# green check on the PR means the suites the review log cites actually ran.

on:
pull_request:
paths:
- "contracts/**"
- "attestor/**"
- "monitor/**"
- "releaser/**"
- ".github/workflows/migration-tests.yml"
push:
branches:
- main

jobs:
contracts:
name: Foundry (contracts)
runs-on: ubuntu-latest
defaults:
run:
working-directory: contracts
steps:
- uses: actions/checkout@v5
with:
submodules: recursive

- uses: foundry-rs/foundry-toolchain@v1
with:
version: stable

- name: Build
run: forge build --sizes

- name: Test
run: forge test -vvv

daemons:
name: ${{ matrix.package }} (node)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package: [attestor, monitor, releaser]
defaults:
run:
working-directory: ${{ matrix.package }}
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: ${{ matrix.package }}/package-lock.json

- name: Install
run: npm ci

- name: Typecheck and test
run: npm test
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@

.DS_Store
.idea
.vscode
.vscode
# Locally generated Zombienet chain spec (13MB, rebuilt by make-zombienet-spec.mjs)
testing/.zombienet-pendulum-raw.json*

# Rehearsal secrets and run artifacts (throwaway keys, but never commit them)
testing/.env.rehearsal
testing/.rehearsal/
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "contracts/lib/openzeppelin-contracts"]
path = contracts/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"pallets/vesting-manager",
"pallets/orml-currencies-allowance-extension",
"pallets/orml-tokens-management-extension",
"pallets/token-migration",
"pallets/treasury-buyout-extension",
"pallets/xcm-teleport",
"runtime/common",
Expand Down
6 changes: 6 additions & 0 deletions attestor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
dist/
checkpoint.json
cp*.json
checkpoint.json.*.tmp
cp*.json.*.tmp
97 changes: 97 additions & 0 deletions attestor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# PEN Migration Attestor

Daemon run by each of the four attestor operators (3-of-4, initially
team-operated — PRD §6.4 / D4).
Watches **relay-finalized** blocks on the operator's **own** Pendulum node for
`tokenMigration.MigrationInitiated` events and submits the matching
`approve(nonce, recipient, palletAmount)` transaction to the MigrationVault on
Base. The vault releases the tokens on the 3rd matching approval; attestors
never communicate with each other — the contract is the only coordination
point.

## Non-negotiable operational rules (PRD A1–A5)

1. **Run your own Pendulum full node** and point `PENDULUM_WS` at it. Using a
public RPC means trusting that RPC with release authority. Run the node
with **`--state-pruning archive`** (or `archive-canonical`): the daemon
catches up block by block through historical state, and a default-pruned
node (256 blocks ≈ 51 min) cannot serve that after any daemon outage
longer than the pruning horizon — the daemon then wedges loudly on
restart. Recovery from that state is pointing `PENDULUM_WS` at an archive
node, never editing the checkpoint.
2. **Key isolation:** the attestor key signs only vault `approve` calls. Keep
it in an HSM/KMS signer where possible; never reuse it elsewhere. The same
address pays gas — keep it funded with Base ETH (the daemon alerts below
`MIN_GAS_BALANCE_WEI`).
3. **Separate infrastructure per operator** — different hosting, different
credentials, nothing shared with other attestors or with the monitor.
4. The daemon **exits on any decode or processing error** instead of skipping
events. Run it under a process manager (systemd example below) and page a
human when it restart-loops: a stuck attestor on a runtime upgrade usually
means the metadata changed and the daemon needs updating.

## Configuration (environment)

| Variable | Meaning |
|---|---|
| `PENDULUM_WS` | WebSocket of your own Pendulum node, e.g. `ws://127.0.0.1:9944` |
| `BASE_RPC_URL` | Base JSON-RPC endpoint |
| `VAULT_ADDRESS` | MigrationVault address on Base |
| `ATTESTOR_PRIVATE_KEY` | This attestor's signing key (0x-prefixed) |
| `CHECKPOINT_FILE` | Path persisting the last processed block (default `./checkpoint.json`) |
| `START_BLOCK` | First Pendulum block to scan on the very first run |
| `MIN_GAS_BALANCE_WEI` | Low-gas alert threshold (default 0.01 ETH) |
| `ALERT_WEBHOOK_URL` | Optional webhook receiving JSON alerts |
| `BASE_CHAIN_ID` | Default 8453 (Base mainnet) |
| `BASE_FINALITY_TAG` | Base confirmation boundary the checkpoint waits for: `safe` (default) or `finalized` |
| `BASE_FINALITY_TIMEOUT_MS` | How long a block's approvals may stay outside that boundary before an alert + idempotent re-submission (default 15 min for `safe`, 45 min for `finalized`) |
| `HEAD_STALL_ALERT_MS` | Page when no finalized Pendulum head has arrived for this long (default 5 min): the daemon is push-driven, so a node that stops finalizing would otherwise idle undetected |

## Run

```sh
npm install
npm run build
npm start
```

### systemd example

```ini
[Unit]
Description=PEN migration attestor
After=network-online.target

[Service]
EnvironmentFile=/etc/pen-attestor/env
WorkingDirectory=/opt/pen-attestor
ExecStart=/usr/bin/node dist/main.js
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
```

## Behavior details

- Blocks are processed strictly in order against the **latest** Base state
(submission, race detection), so throughput is submission latency and a
lost k-of-n race — the most ordinary event in the system — is a log line,
never an alert. The durable **checkpoint trails separately** at the Base
finality boundary: it advances past a block only once every releasable
event in it is resolved inside `safe`/`finalized` state. A crash therefore
re-processes only the blocks whose approvals were not yet durable — safe,
because approvals are idempotent (`nonceConsumed`/`hasApproved` are checked
first, and duplicate submissions revert harmlessly). If a block's approvals
refuse to settle (a reorg dropped them), they are re-submitted after
`BASE_FINALITY_TIMEOUT_MS` with an alert; the checkpoint never passes an
unsettled block.
- Checkpoints are atomically replaced, bound to the Pendulum genesis plus the
configured Base chain and vault, and malformed files are fatal. Never delete or replace one merely to
clear an alert; reconcile it against both chains first.
- The daemon verifies at startup that its address is in the vault's attestor
set and refuses to run otherwise.
- After a Pendulum **runtime upgrade**, verify event decoding against the new
metadata on a staging node before letting the fleet advance past the
upgrade block (see docs/pen-migration-runbooks.md).
Loading
Loading