部署第一个合约(Foundry)
Foundry 是基于 Rust 的高速 Solidity 工具链。本指南覆盖用 forge 把 ERC-20 部署到 OXN 测试网,并解释关于 cast 与机密性的重要注意点。
forge 和 cast 目前不支持 OXN 的加密交易信封。这意味着:
forge create部署可用。 部署交易的 calldata(字节码 + 构造参数)在 OXN 上本就是明文,不需要 wrap。cast send发的交易是明文的。 敏感 calldata 会泄漏到 mempool 和公开区块。任何必须保密的场景,用 Hardhat 或直接用 ethers.js。cast call发的eth_call是明文的。 依赖调用者的 view 函数(例如私密的balanceOf(msg.sender))无法工作——明文调用里msg.sender是零地址。
推荐工作流:用 forge 部署,然后从支持加密调用的 ethers.js 或 Hardhat 脚本发起交互。
前置条件
1. 创建工程
forge init my-oxn-foundry
cd my-oxn-foundry
Foundry 会生成 src/Counter.sol 与测试模板。保留或替换都行。
2. 配置 foundry.toml
编辑生成的 foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.24"
evm_version = "paris" # 必需——OXN 尚未支持 PUSH0
optimizer = true
optimizer_runs = 200
[rpc_endpoints]
oxn_testnet = "https://rpc.bout.network"
[etherscan]
# Sourcify 或浏览器插件式的合约验证待定
evm_version = "paris"——OXN 运行时尚未支持 Shanghai 引入的PUSH0。用了 PUSH0 的合约在运行时会以invalid coderevert。见 EVM 兼容性。- 没有
eth_estimateGas——Foundry 用命令行--gas-limit,显式固定。
3. 安装 OpenZeppelin
forge install OpenZeppelin/openzeppelin-contracts --no-commit
添加 remapping 让 import 解析。项目根下建 remappings.txt:
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
4. 写合约
用 src/MyToken.sol 替换 src/Counter.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
}
}
想让 test/Counter.t.sol 也能编译过就一并更新,或者直接删掉它。
5. 编译
forge build
预期:Compiler run successful!
6. 部署
export PRIVATE_KEY=0x...
forge create src/MyToken.sol:MyToken \
--rpc-url oxn_testnet \
--private-key $PRIVATE_KEY \
--constructor-args 1000000000000000000000000 \
--gas-limit 3000000
--constructor-args 取 1e24(100 万个 18 位小数的代币),按需调整。
预期输出:
Deployer: 0xYourAddress...
Deployed to: 0xNewContractAddress...
Transaction hash: 0x...
部署能成功是因为合约创建交易的 calldata 在 OXN 上本就是明文。部署后合约的状态依然完整加密。
7. 验证部署
查询一个不依赖 msg.sender 的公开函数:
cast call 0xNewContractAddress "totalSupply()(uint256)" --rpc-url oxn_testnet
预期:1000000000000000000000000(或你选的初始供应量)。
8. cast 不能做什么
不要用 cast send 发机密合约调用。 反例:
# 错:明文 calldata,交易金额在公开区块里暴露。
cast send 0xNewContractAddress \
"transfer(address,uint256)" \
0xdead 100 \
--rpc-url oxn_testnet \
--private-key $PRIVATE_KEY
交易照样成功,但参数 (0xdead, 100) 在公开区块里可见。要做机密交互,改用 Hardhat 或带 ethers.js wrapper 的 JS 脚本——见用 Hardhat 部署。
不要用 cast call 触发依赖调用者的 view。 反例:
# 对任何地址返回 0,因为明文 cast call 里 msg.sender 是 0x0。
cast call 0xNewContractAddress "balanceOf(address)(uint256)" 0xYourAddress \
--rpc-url oxn_testnet
标准 ERC-20 的 balanceOf(address who) 能工作,因为它是按地址参数读的(不是 msg.sender)。但一个门控 msg.sender == who 的机密 ERC-20 会返回 0。
桥接方案:Foundry 部署 + JS 交互
一种常见模式:用 Foundry 部署(forge 的构建比 Hardhat 快),然后用 ethers.js wrapper 的 Node.js 脚本做交互:
import { ethers } from "ethers";
import { wrapEthersProvider, wrapEthersSigner } from "@oasisprotocol/sapphire-ethers-v6";
const TOKEN_ADDR = "0xYourDeployedTokenAddress";
const raw = new ethers.JsonRpcProvider("https://rpc.bout.network");
const provider = wrapEthersProvider(raw);
const signer = wrapEthersSigner(new ethers.Wallet(process.env.PRIVATE_KEY, raw));
const abi = ["function transfer(address,uint256) returns (bool)"];
const token = new ethers.Contract(TOKEN_ADDR, abi, signer);
const tx = await token.transfer(
"0x000000000000000000000000000000000000dEaD",
ethers.parseUnits("100", 18),
{ gasLimit: 500_000n }
);
await tx.wait();
console.log("encrypted transfer:", tx.hash);
同时享受 Foundry 的快构建和真正的加密交易。
下一步
- 用 Hardhat 部署——机密交互的推荐路径
- 机密性模型——工具链选择到底影响了什么
- 加密调用 vs 明文调用——为什么
cast发的是明文