Repository navigation
Conversation
ebma
marked this pull request as ready for review
July 8, 2026 07:49
✅ Deploy Preview for rococo-souffle-a625f5 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
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 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; | ||
| } |
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
force-pushed
the
feat/pen-base-migration
branch
from
July 8, 2026 08:00
be6923f to
488ebc2
Compare
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.
5 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_getCodesmart-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) anduseBaseReleaseStatus(polls the vault over plain JSON-RPC).src/helpers/ethereum.ts— EIP-55 checksum, payload-hash mirroring the vault'sabi.encode, minimaleth_call/eth_getCodeclient. No EVM library added — keccak via@polkadot/util-crypto.src/constants/migration.ts— vault address viaVITE_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 theAmountcomponent's currentcontrol/name(react-hook-formController) API. Verified with a fullyarn build(tsc && vite build) againstmain.Verification
yarn buildclean (tsc + vite); committed through the repo's lint-staged hook.Before deploy
Set
VITE_MIGRATION_VAULT_ADDRESS(and optionallyVITE_BASE_RPC_URL) once the vault is deployed on Base.🤖 Generated with Claude Code