Skip to content

Add PEN → Base migration page - #655

Open
ebma wants to merge 8 commits into
mainfrom
feat/pen-base-migration
Open

ebma wants to merge 8 commits into
mainfrom
feat/pen-base-migration

Conversation

@ebma

@ebma ebma commented Jul 8, 2026 •

Copy link
Copy Markdown
Member

Overview

Adds the user-facing PEN → Base migration page to the portal — the frontend companion to the migration stack in pendulum-chain/pendulum#558.

Users connect their Substrate wallet, enter an amount and a Base (EVM) destination, and submit tokenMigration.migrate(...); after finalization the page tracks the release on the Base MigrationVault (attestor approvals x/3 → released).

What's in this PR (2 commits)

  • src/pages/migration/ — the migration page: amount validation (transferable balance, minimum, migrate-all-or-leave-ED), EIP-55 address validation with checksummed preview, zero-address rejection, eth_getCode smart-contract-destination warning + extra confirmation, irreversibility confirmation, pallet-pause banner, locked-balance hint, and post-finalization release tracking.
  • src/hooks/migration/ — useMigrationPallet (extrinsic submission resolving at finality with the emitted nonce) and useBaseReleaseStatus (polls the vault over plain JSON-RPC).
  • src/helpers/ethereum.ts — EIP-55 checksum, payload-hash mirroring the vault's abi.encode, minimal eth_call/eth_getCode client. No EVM library added — keccak via @polkadot/util-crypto.
  • src/constants/migration.ts — vault address via VITE_MIGRATION_VAULT_ADDRESS; the page degrades gracefully when unset. Nav item is Pendulum-only.

Base branch

Rebased onto main (the current React 19 mainline). The amount input uses the Amount component's current control/name (react-hook-form Controller) API. Verified with a full yarn build (tsc && vite build) against main.

Verification

yarn build clean (tsc + vite); committed through the repo's lint-staged hook.

Before deploy

Set VITE_MIGRATION_VAULT_ADDRESS (and optionally VITE_BASE_RPC_URL) once the vault is deployed on Base.

🤖 Generated with Claude Code

@ebma
ebma changed the base branch from fix-issues-with-new-ss58format to main July 8, 2026 07:49
@ebma
ebma marked this pull request as ready for review July 8, 2026 07:49
@netlify

netlify Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for rococo-souffle-a625f5 ready!

Name Link
🔨 Latest commit 1836c07
🔍 Latest deploy log https://app.netlify.com/projects/rococo-souffle-a625f5/deploys/6aa80fbf16ae3c0008f47b43
😎 Deploy Preview https://deploy-preview-655--rococo-souffle-a625f5.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Pendulum-only “PEN → Base” migration flow to the portal, including form validation, Substrate extrinsic submission at finality, and Base-side release-status tracking via JSON-RPC calls to a MigrationVault.

Changes:

  • Introduces the migration page UI + Yup validation schema (amount rules + EIP-55 address validation + user confirmations).
  • Adds hooks for submitting tokenMigration.migrate(...) and polling Base vault release status/approvals.
  • Adds minimal Ethereum helpers (EIP-55 checksum + JSON-RPC eth_call / eth_getCode) and wires the page into routing + navigation.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/pages/migration/ValidationSchema.ts Adds Yup schema for migration form validation (amount + Base address + irreversible confirmation).
src/pages/migration/index.tsx Implements the migration page UI, submission flow, contract-destination warning, and release tracking display.
src/hooks/migration/useMigrationPallet.tsx Adds hook to read pallet constants/paused state and submit the migration extrinsic to finality.
src/hooks/migration/useBaseReleaseStatus.ts Adds hook to poll MigrationVault for approvals/released status of a finalized migration.
src/helpers/ethereum.ts Adds EIP-55 checksum helpers, payload hash encoding, and minimal Base JSON-RPC calls.
src/constants/migration.ts Adds Base target configuration (RPC URL + vault address + explorer URL) and lookup helper.
src/components/Layout/links.tsx Adds a “Migrate to Base” nav item (hidden outside Pendulum).
src/app.tsx Registers the /migration route and page loader.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +102
return new Promise<PendingMigration>((resolve, reject) =>
extrinsic
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.signAndSend(walletAccount.address, { signer: walletAccount.signer as any }, (result) => {
const { status, events } = result;
const errors = getErrors(events, api);

if (status.isInBlock && errors.length > 0) {
reject(new Error(`Transaction failed: ${errors.join('\n')}`));
} else if (status.isFinalized) {
if (errors.length > 0) {
reject(new Error(`Transaction failed: ${errors.join('\n')}`));
return;
}
const migration = extractMigrationInitiated(api, events);
if (migration) {
resolve(migration);
} else {
reject(new Error('MigrationInitiated event not found in finalized transaction'));
}
}
})
.catch((error: Error) => reject(error)),
);
placeholder="0x…"
autoComplete="off"
spellCheck={false}
{...register('baseAddress')}
Comment thread src/helpers/ethereum.ts
Comment on lines +36 to +46
const response = await fetch(baseRpcUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
});
const json = (await response.json()) as { result?: string; error?: { message: string } };
if (json.error || json.result === undefined) {
throw new Error(`Base RPC ${method} failed: ${json.error?.message ?? 'no result'}`);
}
return json.result;
}
ebma added 2 commits July 8, 2026 09:59
New /pendulum/migration page implementing the migration UX (PRD U1-U4):
amount input validated against transferable balance, the minimum
migration amount and the leave-ED-or-migrate-all rule; EIP-55-validated
Base address input with checksummed preview; smart-contract destination
detection via eth_getCode with an extra confirmation; irreversibility
confirmation; pallet pause banner; locked-balance hint pointing at
unstaking; and post-finalization release tracking that polls the
MigrationVault on Base (attestor approvals x/3, released) over plain
JSON-RPC without adding an EVM dependency.

The nav item is Pendulum-only and the Base vault address is configured
via VITE_MIGRATION_VAULT_ADDRESS at deploy time.
The vault refuses a zero recipient, so a burn towards it could never be
released (round-2 audit finding C1); the validator now rejects it before
submission.
@ebma
ebma force-pushed the feat/pen-base-migration branch from be6923f to 488ebc2 Compare July 8, 2026 08:00
ebma added 2 commits July 9, 2026 11:23
Defence-in-depth companion to the pendulum-repo round-6 fix: burning towards
the MigrationVault can never be released (the vault refuses a self-transfer),
and it is the one destination the pallet cannot reject on-chain since it has no
knowledge of Base state. The form now rejects it (case-insensitively) alongside
the zero address, with a unit test covering both.
A single migrate() above the vault's perReleaseCap is burned on Pendulum
but its release on Base is deferred permanently: unlike a daily-cap
deferral it never self-heals, because only a governance setCaps behind
the 48h timelock can clear it. With the largest holder at ~24M PEN
transferable and a cap in the low millions, that is a live foot-gun.

The form now reads perReleaseCap from the vault and, above it, shows a
warning explaining that the release will be held, suggests splitting the
amount into chunks at or below the cap, and requires an explicit
acknowledgement before submission -- mirroring the existing
smart-contract-destination confirmation.
The page said 3-of-5; Deploy.s.sol deploys four attestors with a
threshold of three, as does every other document.
…ting

A release deferred by the vault's daily cap (or a pause) already has all
its approvals. The card said "waiting for attestor approvals (3/3) — a few
minutes", indefinitely. It now says the release is queued and completes
on its own.
The numeric input strips a pasted comma as a thousands separator, so a
pasted "500,5" became 5005 — a 10x over-migration on a one-way path. A
comma is ambiguous between locales, so the migration form refuses the
paste with a hint to use a dot rather than guessing.
Both pre-submission gates keyed on a react-query result being truthy, so
a failed or still-running Base RPC read silently dropped the warning. An
unverifiable destination or cap now needs the same explicit acknowledgement
(with wording that says why), and submission waits while the reads are in
flight. Each acknowledgement is stored with the value it was given for, so
editing the address or the amount retires it instead of carrying it over.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants