部署合约
OXN 上的部署与以太坊几乎一致,只有少量实际调整。本页覆盖生产中真正使用的模式,以及团队第一次踩的坑。
基本要求
OXN 合约部署必须:
- 用
evmVersion: "paris"编译。 - 显式设置
gasLimit(标准合约通常 3–5 百万)。 - 想让未来调用加密就用 wrapped signer——部署本身无论如何都是明文。
- 等待交易入块后再记录地址。
Hardhat 部署脚本
scripts/deploy.js
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deployer:", deployer.address);
console.log("Balance :", (await hre.ethers.provider.getBalance(deployer.address)).toString());
const Contract = await hre.ethers.getContractFactory("MyContract");
const contract = await Contract.deploy(
/* 构造参数 */,
{ gasLimit: 3_000_000n }
);
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log("Deployed:", address);
console.log("Tx hash :", contract.deploymentTransaction().hash);
// 验证代码上链
const code = await hre.ethers.provider.getCode(address);
if (code === "0x") {
throw new Error("部署失败——地址上没有代码");
}
console.log("Code size:", (code.length - 2) / 2, "bytes");
}
main().catch((e) => { console.error(e); process.exit(1); });
跑:
npx hardhat run scripts/deploy.js --network oxn_testnet
Foundry 部署
forge create src/MyContract.sol:MyContract \
--rpc-url oxn_testnet \
--private-key $PRIVATE_KEY \
--constructor-args "arg1" "arg2" \
--gas-limit 3000000
或用脚本:
script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Script.sol";
import "../src/MyContract.sol";
contract DeployScript is Script {
function run() external {
vm.startBroadcast();
new MyContract(/* args */);
vm.stopBroadcast();
}
}
跑:
forge script script/Deploy.s.sol --rpc-url oxn_testnet --broadcast --gas-limit 3000000
确定性部署(CREATE2)
想跨多个环境得到相同合约地址,用 CREATE2:
import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol";
contract Deployer {
function deploy(bytes32 salt, bytes memory bytecode) external returns (address) {
return Create2.deploy(0, salt, bytecode);
}
function computeAddress(bytes32 salt, bytes memory bytecode) external view returns (address) {
return Create2.computeAddress(salt, keccak256(bytecode));
}
}
同样的 salt、bytecode、部署方地址,在任何 EVM 链上都能得到相同合约 地址——用于让多个网络上合约地址一致。
带机密初始状态的构造函数
构造参数是部署 calldata 的一部分,在 OXN 上是明文(字节码 + 参数永远公开)。所以不能通过构造函数初始化机密。
改为部署后通过一次单独的加密调用初始化:
contract Vault {
address public owner;
bytes32 private secret;
constructor(address _owner) {
owner = _owner;
// 不要这样做: secret = _secret; <-- 会公开在构造 calldata 里
}
function setSecret(bytes32 _secret) external {
require(msg.sender == owner, "not owner");
secret = _secret; // 这次调用的 calldata 是加密的
}
}
明文部署,然后通过 wrapped signer 调用 setSecret。机密从此在存储中加密。
代理模式做可升级
OpenZeppelin 的 Transparent 与 UUPS 代理在 OXN 上无需修改即可用。每个代理合约的存储住在代理地址下;存储加密密钥从这个地址派生,因此升级保留加密状态。
Hardhat + OpenZeppelin upgrades
const { upgrades } = require("hardhat");
const Impl = await ethers.getContractFactory("MyContractV1");
const proxy = await upgrades.deployProxy(Impl, [/* init args */], {
initializer: "initialize",
});
await proxy.waitForDeployment();
升级:
const ImplV2 = await ethers.getContractFactory("MyContractV2");
await upgrades.upgradeProxy(proxyAddress, ImplV2);
记录部署地址
不要跨环境硬编码合约地址。推荐:
scripts/deploy.js(带地址持久化)
const fs = require("fs");
const path = require("path");
// ... 部署 ...
const outputPath = path.join(__dirname, "..", "deployments", `${network.name}.json`);
const existing = fs.existsSync(outputPath) ? JSON.parse(fs.readFileSync(outputPath)) : {};
existing.MyContract = {
address: await contract.getAddress(),
txHash: contract.deploymentTransaction().hash,
deployedAt: new Date().toISOString(),
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(existing, null, 2));
console.log("Wrote", outputPath);
把 deployments/oxn_testnet.json(以及其他环境的对应文件)提交到你的仓库。每个开发者和每次 CI 都从这里读最新地址。
常见部署失败
"合约部署了但 getCode 返回 0x。" 交易成功了但代码没落地。几乎总是 gasLimit 太小。提到 5_000_000n 重来。
"Insufficient balance to pay fees。" 领水龙头补 TBLUAI,或测试网上用 gasPrice: 0n。
"合约调用 revert invalid code。" 用错误的 evmVersion 编译了。重 新以 paris 编译。
"部署成功但后续调用报 insufficient balance。" 部署方 gas 够部署但不够后续操作。给账户加钱,或用专门的部署账户。