Ether.fi是一个非托管的流动性再质押协议,用户将ETH质押进去后会收到等值的eETH,资产对应的验证者密钥由用户自己掌控,协议方无法动用用户的本金。对于开发者来说,把一个已有的React去中心化应用接入Ether.fi,核心工作是完成钱包交互、质押合约调用和eETH余额管理这三块逻辑。本文以React配合ethers.js为例,完整走一遍接入流程,并说明迁移过程中容易踩到的坑。

一、eETH的非托管机制是怎么回事
理解eETH的设计,才能在产品文案和前端交互上向用户传达正确的安全边界。与中心化质押不同,用户质押到Ether.fi时,协议会用一组双向加密的验证者密钥启动验证节点:退出密钥(BLS退出凭证)由用户持有,节点运营方只能执行签名职责,无法单方面转移或提取质押的ETH。这就意味着协议方作恶的上限被锁死了,这正是非托管三个字的实际含义。
eETH本身是ERC-20标准的再质押代币,采用rebase机制,用户钱包里的eETH数量会随质押收益自动增长,不需要手动claim。这一点对接前端有直接影响:页面上展示的余额是实时变动的,你不需要额外写一套收益结算逻辑。另外,Ether.fi在流动性池(LiquidityPool合约)层面保留了即时赎回通道,用户可以用eETH换回ETH,也可以通过池子做即时交易,这决定了我们在前端需要同时处理质押与赎回两个方向的合约调用。
需要提醒一点:eETH的授权模型与普通ERC-20略有差异。质押ETH进入LiquidityPool时,用户是先调用deposit()函数拿到一个凭证,再调用withdraw()铸造eETH,而不是直接approve加transferFrom的流程。下面写代码时会体现这个细节。
二、在React项目中搭建接入环境
假设你的项目已经用Vite或CRA建好,第一步是安装依赖。除了ethers这个基础库,建议同时安装wagmi和viem的组合,因为Ether.fi官方文档中的合约地址和ABI引用方式与两者都兼容,而wagmi的hooks写法能大幅减少React中的状态管理代码。
npm install ethers viem wagmi @tanstack/react-query
接着配置链与合约常量。Ether.fi主部署在以太坊主网,同时支持Arbitrum等L2上的eETH流通。合约地址以官方文档为准,这里以LiquidityPool为例:
export const LIQUIDITY_POOL = "0x308867A101ba337fEA8a6f748c4670a0Bc42d511"; export const EETH_TOKEN = "0x35fA164735182de50811E8e2E824cFb9B6114c19"; export const liquidityPoolABI = [ "function deposit() external payable returns (uint256)", "function withdraw(address recipient) external returns (uint256)" ]; export const eethABI = [ "function balanceOf(address account) external view returns (uint256)", "function approve(address spender, uint256 amount) external returns (bool)" ];
这里用最小化ABI(human-readable格式)而不是完整的JSON ABI,好处是体积小、可读性高,ethers.js会自动解析。如果你的项目需要监听事件或调用更多方法,可以从官方仓库拉取完整ABI再做裁剪。地址一定不要硬编码在组件里,集中放到一个constants文件,方便后续多链扩展和测试网切换。
三、实现质押与赎回的核心交互
钱包连接部分如果你已经在用wagmi的useAccount和useSigner,可以直接复用。核心的质押流程分两步:先deposit存入ETH,再withdraw领取eETH。注意两步之间要等待第一笔交易确认,否则第二笔会revert。
import { ethers } from "ethers";
import { LIQUIDITY_POOL, liquidityPoolABI } from "./constants";
export async function stakeETH(signer, amountInEth, onSuccess) {
const pool = new ethers.Contract(LIQUIDITY_POOL, liquidityPoolABI, signer);
const amount = ethers.parseEther(amountInEth);
// 第一步:存入ETH,拿到质押凭证
const tx1 = await pool.deposit({ value: amount });
await tx1.wait(1);
// 第二步:提取eETH到用户钱包
const tx2 = await pool.withdraw(await signer.getAddress());
const receipt = await tx2.wait(1);
onSuccess(receipt);
}在React组件中调用时,务必处理用户拒签、Gas不足和滑点三类异常。用户拒签时ethers会抛出带ACTION_REJECTED标识的错误,应该静默处理而不是弹红色报错;而链上revert则要解析revert reason并转成中文提示。下面是一个带有状态管理的组件示例:
function StakePanel() {
const { data: signer } = useSigner();
const [amount, setAmount] = useState("");
const [status, setStatus] = useState("idle");
async function handleStake() {
setStatus("pending");
try {
await stakeETH(signer, amount, () => setStatus("done"));
} catch (err) {
setStatus(err.code === "ACTION_REJECTED" ? "cancelled" : "failed");
}
}
return (
<div>
<input value={amount} onChange={e => setAmount(e.target.value)} />
<button onClick={handleStake} disabled={status === "pending"}>
{status === "pending" ? "处理中..." : "质押并获取eETH"}
</button>
</div>
);
}赎回方向稍微复杂一些。eETH换回ETH可以走LiquidityPool的即时赎回,前提是池子流动性充足;流动性紧张时需要通过协议的退出队列等待验证者提款。前端应该同时读取池子的可用流动性(totalValueLocked相关视图函数),当用户的赎回量超过池子缓冲时,主动提示用户可能存在等待期,避免交易直接失败带来的困惑。
四、余额展示、事件订阅与迁移注意事项
eETH是rebase代币,余额展示建议用useBalance或自己封装的轮询hook,每出一个块刷新一次即可,不需要长轮询。订阅Transfer事件时要注意:eETH的rebase增发会触发大量事件,不要在组件里全量监听,只监听当前地址相关的日志,并在组件卸载时清理listener,否则反复挂载组件会造成内存泄漏和重复RPC请求。
迁移阶段还有几个实际经验值得记录。第一,RPC节点要选支持WebSocket的供应商,事件订阅才稳定,纯HTTP轮询在高并发区块时会漏事件。第二,金额输入要做小数位校验,eETH展示精度建议保留6到8位,内部计算全程用BigNumber,任何一环节转成Number都会丢精度。第三,如果你原来的应用已经接入Lido的stETH,界面结构可以复用,但要把两步式交易(deposit加withdraw)的流程差异体现到进度提示里,用户对中间多一次签名是有感知的。第四,主网Gas价格波动大,质押两笔交易建议给用户展示预估总成本,可以用provider.estimateGas结合getFeeData计算。
最后,上线前建议在Sepolia测试网走一遍完整流程,Ether.fi的测试环境允许小额验证合约交互是否符合预期。整体来看,React应用接入Ether.fi的工作量主要集中在合约封装层,UI层与普通DeFi应用差别不大,把非托管的交互语义讲清楚,剩下的就是扎实的异步状态管理了。