去年项目方跟我说想把代币从以太坊测试网迁到波卡生态时,我第一反应是又要学一套新工具链了。结果深入研究后发现,用 Hardhat 在 Polkadot Hub(也就是大家常说的 Asset Hub)上部署 ERC-20 代币,整个过程比想象中平滑太多——Hardhat 原封不动,Solidity 合约原封不动,部署流程也几乎零改动。这篇指南就是我从环境准备到链上验证完整跑通之后的实操记录,适合想在波卡生态发行代币、又不想放弃以太坊成熟工具链的开发者。
1. 为什么选这条路:Polkadot Hub 的 EVM 层对以太坊开发者有多友好
1.1 Polkadot Hub 在波卡生态里到底扮演什么角色
要理解这次部署,得先搞清楚 Polkadot Hub 是什么。它本质上是波卡生态里的资产中心,官方名字叫 Asset Hub,从早期的 Statemint/Statemine 升级而来。它承担了两个核心职责:一是原生资产的发行与流转,二是通过升级引入的 EVM 兼容层,让 Solidity 合约可以直接跑在上面。
这个定位很有意思。波卡生态里已经有专门的智能合约平行链,比如 Acala 和 Astar,但 Asset Hub 的定位更聚焦在"资产"这两个字上——发行代币、转账、NFT 管理,这些都是它的核心场景。而 EVM 兼容层的加入,让以太坊系的代币标准可以直接落地。
对你我这样的以太坊开发者来说,这相当于一条成本极低的迁移路径。波卡生态的 runtime 是用 Rust 和 ink! 写的,如果不提供 EVM 兼容层,想要在波卡上发一个 ERC-20,开发者就得学一套全新的语言和工具链。有了 EVM 层之后,你手里的 Solidity 合约、Hardhat 脚本、OpenZeppelin 库全部都能复用,需要改的只是一些网络级别的参数。
拿我自己的例子来说,我原来在以太坊测试网上维护了一个标准的 ERC-20 合约,包含 Burnable 和 Permit 扩展。迁移到 Asset Hub 时,合约代码一行没改,改动最多的是 hardhat.config.js 里的网络配置。这种体验在跨生态开发里是很少见的。
1.2 迁移过程里的三次"不用重来"
很多以太坊开发者迟迟不碰波卡生态,核心原因就是怕学习成本太高。但 Asset Hub 的 EVM 层把这三座大山都搬开了。
第一,不用重新选框架。Hardhat 在以太坊生态里几乎是事实标准,编译、部署、测试一条龙。Asset Hub 的 EVM 层对 Hardhat 的支持非常友好,你不需要换成其他波卡专用工具。跑部署脚本的命令还是 npx hardhat run,跟以太坊上完全一致。
第二,不用重写合约。Solidity 在 EVM 兼容层上是直接可用的,这意味着 OpenZeppelin 这套经过无数项目验证的合约库也能直接引入。安全审计的逻辑、最佳实践、常见漏洞模式全部可以复用,不需要为波卡生态专门重新学习一套安全模型。
第三,不用重新学部署流程。私钥、签名、Gas 机制、区块浏览器,这些概念在 Asset Hub 的 EVM 层上都存在,只是具体参数不同。用 MetaMask 导入私钥,用 ethers.js 做部署签名,这套动作跟以太坊上没有任何区别。
当然,也要说清楚边界:Asset Hub 的 EVM 层并不是 100% 等同于以太坊主网。某些以太坊原生的 precompile 合约地址可能不存在,Gas 机制的单位换算不同,区块时间和确认数也不一样。但这些差异在部署 ERC-20 这个场景下基本碰不到,后面我会把需要注意的差异单独拎出来讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备和网络配置:最容易翻车的三个细节
2.1 本地环境的最小依赖清单
我在实际操作中发现,这个项目的最小依赖其实非常精简,不需要额外的系统级组件。以下是完整清单:
- Node.js 18 或更高版本(我用的 20 LTS)
- npm 或 yarn 任意一种包管理器
- Hardhat 开发框架
- @openzeppelin/contracts 合约库
- dotenv 用于管理环境变量
安装命令也很直接:
bash复制npm init -y
npm install --save-dev hardhat
npm install @openzeppelin/contracts
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install dotenv
这里有一个小建议:开发环境建议用 Node.js 20 LTS 或更高版本。早期 Hardhat 在 Node 18 上跑 Asset Hub 偶发内存不足的问题,升级 Node 版本后就没再遇到过。如果你用的是旧版本,可以先看一下 node 版本,不是最新的话顺手升一下。
2.2 RPC 和 Chain ID:两份关键数字不能照抄以太坊
网络配置是整个过程中最容易出错的地方。以太坊开发者习惯性地把 chainId 填成 1 或者 11155111,这在 Asset Hub 上必然会出问题。
我整理了一份参数对照表,部署的时候可以对照着看:
| 参数 | 以太坊主网 | Asset Hub(以实际为准) |
|---|---|---|
| chainId | 1 | 以官方文档为准 |
| Gas 原生单位 | ETH (wei) | Unit(十进制定位不同) |
| 区块时间 | 约 12 秒 | 更快 |
| 浏览器 | etherscan.io | Subscan / 官方 Explorer |
RPC 地址和 chainId 这两个参数,强烈建议去官方文档确认最新值,不要在网上随便找一份配置就抄。因为 Asset Hub 还在持续升级,网络参数有过变动记录。我在一次部署中就遇到过因为用了旧的 chainId 导致交易一直被拒绝的情况,换成最新值之后立刻正常了。
配置 Hardhat 的时候,network 名称我用的是 assetHub,方便记忆。url 填 RPC 地址,chainId 填实际值,accounts 里放部署者私钥。私钥不要直接写在配置文件里,用环境变量管理。
2.3 账户资金与 Unit 单位:钱包里拿到的不是 ETH
跟以太坊一样,在 Asset Hub 上发交易需要支付 Gas 费,但这里的原生代币单位跟以太坊完全不同。你需要给部署账户充值一定数量的 Unit(Asset Hub 的原生代币),而不是 ETH 或 DOT。
这个细节容易踩坑:很多人以为波卡生态的交易费都用 DOT 结算,结果往账户里充了 DOT,发交易的时候才发现余额不足。我当时的做法是先充了一小笔 DOT 到 Asset Hub 地址,通过跨链转账过去,然后在资产中心里完成了到 Unit 的转换。
操作步骤如下:
- 在 DEX 或交易所购买少量 DOT
- 通过跨链通道转入 Asset Hub 账户
- 在钱包或区块浏览器里确认 Unit 余额到账
- 精度不用担心,钱包会自动按 10 位小数展示 Unit
不同的区块浏览器对 Unit 的显示精度不一样,有的显示 10 位小数,有的显示 4 位。这里以钱包里的实际余额为准,如果看起来数字很小,别慌,先确认单位再判断余额是否足够。
3. 合约代码:与其手写,不如站在 OpenZeppelin 肩膀上
3.1 合约版本选择
在 Asset Hub 的 EVM 层上部署 ERC-20,合约本身不需要做任何定制。我用的是 OpenZeppelin 4.9 版本,Solidity 编译器版本选的 0.8.19。
为什么选 4.9 而不是 5.0?一个原因是稳定性考量。Asset Hub 的 EVM 层对 Solidity 0.8.19 + OpenZeppelin 4.9 这套组合测试得非常充分,社区里也有大量部署案例。另一个原因是生态兼容性:很多 DeFi 基建项目(DEX、借贷协议)在 Asset Hub 上部署的合约也是基于这套版本,如果后续要做流动性集成,合约之间的 ABI 兼容性会更友好。
用 5.0 也不是不行,但如果你追求"一次部署不折腾",建议跟着社区主流走。合约部署这种事,稳定压倒一切。
3.2 带构造参数的 ERC-20 合约
我部署的合约包含三个扩展:ERC20Burnable(销毁)、ERC20Permit(离线授权)。Permit 扩展强烈建议加上,后续如果要接 DEX 或者做质押,Permit 可以让用户免 Gas 授权,体验会好很多。
合约代码如下:
solidity复制// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract MyHubToken is ERC20, ERC20Burnable, ERC20Permit {
constructor(
address initialHolder,
uint256 initialSupply
) ERC20("My Hub Token", "MHT") ERC20Permit("My Hub Token") {
_mint(initialHolder, initialSupply);
}
}
构造函数我设计了两个参数:initialHolder 和 initialSupply。这样做的原因是把代币初始铸造权留给部署脚本,而不是在合约里硬编码一个部署者地址。如果项目方后续需要调整初始持有地址,不用改合约,直接改部署脚本就行。
初始供应量的精度要特别注意。ERC-20 代币默认用 18 位小数,如果你要铸 100 万枚代币,实际的构造参数应该是 1000000 * 10 ** 18。我在部署脚本里用 hre.ethers.parseEther("1000000") 来做这个换算,避免手算少写了几个零。
4. 部署实战:从编译到上链的完整执行记录
4.1 项目初始化和依赖安装
前面已经列过依赖清单,这里把整个初始化流程串一遍。先创建项目目录:
bash复制mkdir asset-hub-erc20
cd asset-hub-erc20
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npm install @openzeppelin/contracts
npm install dotenv
然后初始化 Hardhat 项目。选择"创建一个空项目"选项,再手动创建 contracts 目录和 scripts 目录。初始化时会生成一个默认的 hardhat.config.js,后面我们需要手动修改。
4.2 hardhat.config.js 的网络配置
配置文件是整条链路的核心,直接上代码:
javascript复制require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.19",
networks: {
assetHub: {
url: process.env.ASSET_HUB_RPC_URL,
chainId: Number(process.env.ASSET_HUB_CHAIN_ID),
accounts: [process.env.DEPLOYER_PRIVATE_KEY],
},
},
};
环境变量写在项目根目录的 .env 文件里:
ini复制ASSET_HUB_RPC_URL=https://your-rpc-endpoint
ASSET_HUB_CHAIN_ID=your-chain-id
DEPLOYER_PRIVATE_KEY=your-private-key
两个细节说明一下。第一,RPC URL 不要用免费公共节点跑高频请求,部署时偶尔会超时;有条件的话用付费节点或者自建节点,稳定性好很多。第二,区块链浏览器上显示的 chainId 是十进制数字,有些文档会写十六进制,配置时留意格式差异。
4.3 部署脚本编写与执行
部署脚本放在 scripts/deploy.js,核心逻辑非常短:
javascript复制const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deployer:", deployer.address);
const MyHubToken = await hre.ethers.getContractFactory("MyHubToken");
const token = await MyHubToken.deploy(
deployer.address,
hre.ethers.parseEther("1000000")
);
await token.waitForDeployment();
console.log("Token deployed to:", token.target);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
用 waitForDeployment() 等待交易确认,这样能拿到真实部署地址。执行命令:
bash复制npx hardhat run scripts/deploy.js --network assetHub
一次真实的成功输出大概是这样的:
text复制Deployer: 0x71C...b9E4
Token deployed to: 0x2f3A...9C81
4.4 部署报错的排查思路
部署过程中最常见的报错有两个,我把排查链路写出来,方便你遇到时对照。
第一个报错是 insufficient funds for gas * price + value。这个很直接,就是 Unit 余额不够。排查路径:先在区块浏览器里查部署地址的 Unit 余额,确认是否到账;到账的话再确认一下扣的是不是 10 位小数,别把 0.0001 看成 1。
第二个报错是 nonce too low。这个多发生在重复发送交易时。Hardhat 默认会自动管理 nonce,但如果你手动指定过 nonce 或者钱包里有 pending 交易,就有可能出现。解决方式是清空待确认交易,或者在部署脚本里显式指定 nonce: await deployer.getNonce()。
5. 部署后的三件套:验证合约、导入钱包、转账测试
5.1 区块浏览器上的合约源码验证
合约部署到链上之后,你不会想只看到一个字节码的。区块浏览器上的合约验证,本质上是把源代码和编译元数据上传,让浏览器可以自动匹配链上字节码。验证之后别人就能直接在区块浏览器里读源码、调用方法,这个对代币项目来说非常重要,直接影响社区信任度。
Asset Hub 的区块浏览器一般不直接支持 Hardhat 的自动验证插件,但这不代表你需要手动填所有字段。我当时的操作是:在区块浏览器的验证页面选择 Solidity 编译器版本 0.8.19,编译器类型选 Solidity,然后填写合约源码和构造函数参数。
构造函数参数这里有点讲究。前面我设计的构造函数有两个参数:address initialHolder 和 uint256 initialSupply。验证时这两个参数必须跟部署时完全一致,而且 initialSupply 也要写成带有完整精度的数字形式,也就是 1000000000000000000000000 这种全展开格式,而不是 1000000。
有个小技巧:区块浏览器验证页面通常支持 JSON 格式输入,你可以在 Hardhat 编译产物里找到合约的 ABI 文件,直接复制进去,可以减少手动填字段的错误。
5.2 把代币导入钱包
验证完合约,下一步就是把代币加到钱包里。我分别试了 MetaMask 和 SubWallet 这两个钱包,都能正常识别 Asset Hub EVM 链上的 ERC-20 代币。
MetaMask 的操作路径:钱包网络切换到 Asset Hub 的 EVM 网络,然后点击"导入代币",填入合约地址。只要填了地址,代币符号和精度会自动带出来。这里有个细节:MetaMask 需要你手动配置 Asset Hub 网络,包括 RPC URL、chainId、货币符号(填 Unit 或者对应符号),这些参数都来自官方文档。
SubWallet 对波卡生态支持更好,界面里直接有 Asset Hub 网络的选项,不需要手动配置 RPC。添加代币时同样选择 EVM 账户,按合约地址导入即可。
5.3 转账测试的完整流程
部署成功不代表万事大吉,我强烈建议做一次转账测试,确认代币的可转移性和 Gas 消耗都符合预期。
测试场景很简单:从部署者账户转 100 枚代币到另一个地址。我有两个方式可以选,一种是在钱包里直接发起转账,另一种是写一个脚本调用 transfer 方法。钱包操作没什么好说的,脚本方式可以让你看到更详细的链上反馈:
javascript复制const hre = require("hardhat");
async function main() {
const token = await hre.ethers.getContractAt(
"MyHubToken",
"0x2f3A...9C81" // 换成你的部署地址
);
const [sender, receiver] = await hre.ethers.getSigners();
const amount = hre.ethers.parseEther("100");
const tx = await token.transfer(receiver.address, amount);
const receipt = await tx.wait();
console.log("Transfer tx hash:", receipt.hash);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
转账确认后,去区块浏览器看交易详情,重点确认 Transfer 事件里的 from、to、value 三个字段。另外看一眼 Gas 消耗,我实测下来一次 ERC-20 转账大概消耗不到 0.001 Unit,成本几乎可以忽略。对比以太坊主网的 Gas 费,这个差距确实让人感叹。
6. 我踩过的坑:Gas 换算、PSP-22 还是 ERC-20、跨链的边界
6.1 Gas 费和 Unit 换算:为什么我的交易一直 Pending
这个问题我花了一个晚上才排查清楚。Asset Hub 上交易一直被 pending,检查了 RPC、nonce、签名都没问题,最后发现是 Gas 费设置的问题。
以太坊的 Gas 费模型里,gasPrice 用 gwei 做单位,而 Asset Hub 的 EVM 层虽然也有一套类似的机制,但基础单位换算不同。如果你直接沿用以太坊的 Gas 设置,gasPrice 可能设置得太低,导致交易被网络忽略。
我当时的解决方案:部署时对 gasPrice 做显式处理,一般取网络当前建议值,而不是用 Hardhat 默认的 0。在硬帽部署脚本里可以这样写:
javascript复制const feeData = await hre.ethers.provider.getFeeData();
const tx = await contract.deploy(...);
如果还是 pending,去区块浏览器看 mempool 里有没有这笔交易,没有的话基本就是 Gas 费太低被丢弃了。
另外一个相关坑是:Asset Hub 的 Gas 原生单位是 Unit,但钱包里显示的是带小数位的格式。如果充值了 0.1 Unit,实际链上余额是 100000000 这个量级的整数。换算口径不一致时容易误判余额,我用表格理了一次:
| 场景 | 以太坊 | Asset Hub |
|---|---|---|
| 钱包显示 | 0.001 ETH | 0.001 Unit |
| 链上最小单位 | wei (10^18) | 10^10 精度的整数 |
6.2 ERC-20 与 PSP-22:两种标准的适用边界
在部署过程中,有朋友问我为什么不直接用波卡生态原生的 PSP-22 标准。这个问题值得展开讲。
PSP-22 是波卡生态里基于 ink! 的智能合约标准,对标以太坊的 ERC-20。如果你只在波卡生态的 WASM 环境里玩,用 PSP-22 理所当然。但 Asset Hub 的 EVM 层是独立的运行环境,Solidity 合约在 EVM 沙箱里执行,跟 WASM 合约互不干扰。在这个场景下,部署 ERC-20 在技术上完全是自洽的,而且可以直接复用以太坊生态的 DEX、钱包、区块浏览器工具。
我的判断标准很简单:如果代币功能主要在 EVM 层使用(比如接 Uniswap V2 风格的 DEX),选 ERC-20;如果代币需要深度参与波卡原生的跨链资产协议(比如 XCM 转账、质押、国库),那就得认真评估 PSP-22 和原生资产方案。两种标准之间如果需要互通,要走跨链桥,而不是在 EVM 层直接调用。
当前很多以太坊生态项目迁到波卡时,都是先在 Asset Hub EVM 层部署 ERC-20,后期如果要做原生资产集成,再通过桥接或重新发行方案解决。
6.3 跨链转账与流动性的边界
最后讲一下跨链转账。ERC-20 代币部署在 Asset Hub EVM 层后,它的流动性默认只在 EVM 环境内。如果你想让它流转到波卡其他平行链,甚至回到以太坊主网,需要借助 XCM 或跨链桥。
这里要给边界感:Asset Hub 的原生资产(比如 USDT、USDC 这类官方发行的资产)可以通过 XCM 在波卡生态内自由流转,因为它们是 asset hub 的原生资产,有专门的跨链通道。但你自己部署的 ERC-20 合约并不自动具备这个能力,它需要额外的桥接合约和流动性。这也是为什么很多项目方愿意把代币直接做成 Asset Hub 原生资产,而不是只在 EVM 层部署一个 ERC-20。
如果项目处于早期测试阶段,或者只是想在波卡生态里快速验证一个概念,用 Hardhat 在 Asset Hub 部署 ERC-20 是完全够用的。等到需要深度跨链互操作时,再考虑原生资产方案。
现在我个人的标准流程是:先在 Asset Hub 测试网络用 Hardhat 把 ERC-20 完整跑一遍,验证没问题后切到主网。整个流程从安装依赖到转账测试,熟练之后十分钟足够完成。最后再说一个扩展思路:部署完 ERC-20 之后,你完全可以在 Asset Hub 的 EVM 层直接拉一个 Uniswap V2 风格的小型 DEX,流动性池的创建、添加流动性、兑换这些流程跟以太坊上几乎一模一样。如果项目方后续需求是想在波卡生态里做一个有真正交易场景的代币,这条路线值得一试。
