Token Migration UI Development
Token migration is one of the few cases in Web3 where UX is critical from a security perspective. A user approves a contract they may not fully trust. The UI task: make every step transparent, show exactly what happens to the tokens, and prevent the user from clicking "Approve" without understanding the consequences.
Migration flow
Standard scheme: old token (v1) → new token (v2) through a migrator contract. The contract accepts v1, burns or locks it, mints v2 in a 1:1 ratio (or another).
The UI should cover three transactions:
-
approve(migratorContract, amount)on the v1 token -
migrate(amount)on the migrator contract - (optional) add v2 to MetaMask via
wallet_watchAsset
async function migrateTokens(amount: bigint) {
// Step 1: check current allowance
const currentAllowance = await v1Token.allowance(userAddress, MIGRATOR_ADDRESS);
if (currentAllowance < amount) {
const approveTx = await v1Token.approve(MIGRATOR_ADDRESS, amount);
await approveTx.wait();
}
// Step 2: migration
const migrateTx = await migrator.migrate(amount);
const receipt = await migrateTx.wait();
return receipt;
}
Key UI components
Balance display before/after
Show the user explicitly what will happen: how much v1 will be deducted, how much v2 they'll receive. If the rate isn't 1:1 — especially important.
function MigrationPreview({ amount, exchangeRate }: Props) {
const v2Amount = (BigInt(amount) * BigInt(exchangeRate * 100)) / 100n;
return (
<div className="migration-preview">
<div className="from">
<span>Giving: {formatEther(amount)} {V1_SYMBOL}</span>
</div>
<ArrowIcon />
<div className="to">
<span>Receiving: {formatEther(v2Amount)} {V2_SYMBOL}</span>
</div>
</div>
);
}
Stepper with transaction states
Each step — approve, migrate, done — is displayed with explicit status: waiting, pending, confirmed, error.
type MigrationStep = 'idle' | 'approving' | 'approved' | 'migrating' | 'done' | 'error';
const stepConfig = {
idle: { label: 'Ready for migration', icon: 'clock' },
approving: { label: 'Confirming approval...', icon: 'spinner' },
approved: { label: 'Approval confirmed', icon: 'check' },
migrating: { label: 'Migrating tokens...', icon: 'spinner' },
done: { label: 'Migration complete', icon: 'check-circle' },
error: { label: 'Error', icon: 'x-circle' },
};
Link to Etherscan for each transaction as soon as the txHash is received — don't wait for confirmation.
Infinite approve warning
If the contract requests type(uint256).max approve — explicitly tell the user. Offer a choice: exact amount or unlimited. For migration, the right choice is exact amount — the user is migrating a specific quantity.
Handling edge cases
Partial migration: user migrated part of the tokens, returned later. Show current v1 and v2 balances, remainder to migrate.
Migration deadline: migrator contracts often have a deadline after which migration is impossible. If a deadline is set — show countdown, warn in advance (7 days, 24 hours).
Revert reasons: if a transaction reverts — try to decode the reason via parseRevertReason and show a human-readable message instead of "Transaction failed".
function parseRevertReason(error: any): string {
const message = error?.info?.error?.message || error?.message || '';
if (message.includes('Migration ended')) return 'Migration period has ended';
if (message.includes('Insufficient balance')) return 'Insufficient tokens';
if (message.includes('user rejected')) return 'Transaction rejected by user';
return 'Unknown error. Try again later.';
}
Timeline estimates
Full migration UI with stepper, balances, transactions, and error handling — 2-3 days.







