基于 Foundry 的 ERC20 工程,合约支持 UUPS 升级,使用 CREATE2 确保代理合约在主网与测试网部署在同一地址。
用户/应用
│
▼
ERC1967Proxy ←── CREATE2 部署,地址跨链不变(这是代币的"永久地址")
│
│ delegatecall
▼
MyTokenV1 / MyTokenV2 ←── 实现合约,逻辑在这里,可被升级替换
- 代理合约:持有所有状态(余额、授权等),地址永不变
- 实现合约:只含逻辑,升级时部署新版本,代理切换指向即可,状态不受影响
src/
MyTokenV1.sol # V1 实现:标准 ERC20 + mint + burn
MyTokenV2.sol # V2 实现:新增 maxSupply 总量上限(升级示例)
script/
DeployMyToken.s.sol # 首次部署(impl + proxy,均走 CREATE2)
UpgradeMyToken.s.sol # 升级(V1 → V2)
test/
MyToken.t.sol # 单元测试(ERC20 基础 + 升级 + CREATE2 地址预测)
.env.example # 环境变量模板
foundry.toml # Foundry 配置
CREATE2 地址由以下公式确定,与"谁来部署"、"在哪条链"无关:
proxy_addr = keccak256(
0xff
++ 0x4e59b44847b379578588920cA78FbF26c0B4956C // Nick's factory,所有链相同
++ proxy_salt
++ keccak256(ERC1967Proxy_initcode + impl_addr + initialize_calldata)
)[12:]
保证地址一致需固定以下三项:
| 参数 | 说明 |
|---|---|
PROXY_SALT |
代理 salt,一旦确定不要修改(脚本有默认值) |
TOKEN_OWNER |
每条链必须填写同一个地址 |
| 代币参数 | TOKEN_NAME / TOKEN_SYMBOL / INITIAL_SUPPLY 不能变 |
impl salt 无需配置,脚本自动由
keccak256(MyTokenV1.creationCode)派生。
curl -L https://foundry.paradigm.xyz | bash && foundryupgit clone <repo>
cd <repo>
forge installforge test -vcp .env.example .env
# 编辑 .env,填写 PRIVATE_KEY、RPC_URL、TOKEN_OWNER 等
source .envforge script script/DeployMyToken.s.sol --rpc-url $RPC_URLforge script script/DeployMyToken.s.sol \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEYforge script script/DeployMyToken.s.sol \
--rpc-url $MAINNET_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY脚本内置幂等保护:如果目标地址已有代码(重复执行),直接跳过,不报错。
将代理从 V1 升级到 V2。V2 新增了可选的 maxSupply 总量上限。
# 不设上限
PROXY_ADDRESS=0x... \
forge script script/UpgradeMyToken.s.sol \
--rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast
# 设置上限为 2,000,000 个代币
PROXY_ADDRESS=0x... MAX_SUPPLY=2000000 \
forge script script/UpgradeMyToken.s.sol \
--rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast升级后可通过 version() 确认:
cast call $PROXY_ADDRESS "version()(string)" --rpc-url $RPC_URL
# 返回 "2.0.0"| 函数 | 权限 | 说明 |
|---|---|---|
initialize(name, symbol, supply, owner) |
仅初始化一次 | 替代 constructor |
mint(address to, uint256 amount) |
onlyOwner | 增发 |
burn(uint256 amount) |
任何人 | 销毁调用者的代币 |
upgradeToAndCall(address impl, bytes data) |
onlyOwner | 升级实现合约 |
version() |
view | 返回 "1.0.0" |
| 函数 | 权限 | 说明 |
|---|---|---|
initializeV2(uint256 maxSupply) |
onlyOwner,仅调用一次 | 设置总量上限 |
mint(address to, uint256 amount) |
onlyOwner | 增发,超过 maxSupply 时 revert |
maxSupply() |
view | 查询上限(0 表示无上限) |
version() |
view | 返回 "2.0.0" |