Compound是以太坊生态中最具代表性的借贷协议之一,它允许用户向资金池供应资产赚取利息,也可以抵押资产借出其他代币。如果一个已经存在的React借贷应用还停留在中心化的账本逻辑上,迁移到Compound协议不仅能获得真实的市场利率和去中心化的清算机制,还能借助其Governance模块让社区参与协议参数的决策。本文将从依赖准备、合约交互、治理集成三个层面,完整梳理迁移过程中的关键步骤和容易踩到的坑。

迁移前的技术准备与依赖梳理
迁移的第一步不是直接改代码,而是梳理现有应用与Compound协议之间的差异。你需要明确现有借贷应用的核心功能有哪些:充值、提现、借入、还款、利息计算、清算逻辑。在Compound中,这些功能分别对应供应(mint)、赎回(redeem)、借入(borrow)、还款(repayBorrow),而利率计算和清算完全由链上合约自动完成,前端不再需要自己维护一套利率模型,这一点是迁移后最大的架构简化。
在依赖方面,推荐使用ethers.js作为Web3交互库,配合wagmi或者直接用ethers的BrowserProvider连接MetaMask等钱包。项目安装依赖如下:
npm install ethers @compound-finance/compound-js # 或者使用wagmi构建更现代的hooks风格 npm install wagmi viem @tanstack/react-query
另一个必须提前确认的事项是网络环境。Compound V2部署在以太坊主网,同时也有以太坊测试网的历史部署记录。开发阶段建议先在本地用Hardhat或Anvil fork主网,这样可以用真实的合约状态进行调试,避免在测试网找不到完整的市场配置。fork主网的命令示例:
anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/你的API_KEY --block-number 19000000
fork之后,你的本地节点上就有真实的cToken合约、Comptroller合约和COMP代币,前端只需要把RPC地址指向本地节点即可联调,这是迁移阶段效率最高的做法。
在React中接入Compound合约:地址映射与核心操作
Compound V2的核心合约包括Comptroller(风险控制与账户流动性)、各个市场的cToken(如cETH、cDAI)、PriceOracle(价格预言机)。前端需要维护一份地址映射表,建议单独抽出一个配置文件,按网络ID区分:
// src/config/compound.js
export const COMPOUND_ADDRESSES = {
1: {
comptroller: '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B',
comp: '0xc00e94Cb662C3520282E6f5717214004A7f26888',
cEth: '0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5',
cDai: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643'
},
31337: {
// 本地fork主网时使用主网地址
comptroller: '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B',
comp: '0xc00e94Cb662C3520282E6f5717214004A7f26888'
}
};供应操作方面,Compound V2区分了ERC20和ETH两种路径。供应ERC20时需要先调用底层代币的approve授权给cToken合约,再调用mint;供应ETH则直接向cETH合约发送交易并附带value。下面是一个封装好的供应函数示例:
async function supplyErc20(signer, erc20Address, cTokenAddress, amount) {
const erc20 = new ethers.Contract(
erc20Address,
['function approve(address,uint256) returns (bool)'],
signer
);
const cToken = new ethers.Contract(
cTokenAddress,
[
'function mint(uint256) returns (uint256)',
'function balanceOf(address) view returns (uint256)',
'function exchangeRateCurrent() view returns (uint256)'
],
signer
);
const tx1 = await erc20.approve(cTokenAddress, amount);
await tx1.wait();
const tx2 = await cToken.mint(amount);
await tx2.wait();
}借入操作前必须先调用Comptroller的enterMarkets,把抵押的cToken纳入流动性计算,否则账户没有借款额度。这个步骤很多迁移者会遗漏,导致borrow交易revert却查不到原因。借入和还款的实现如下:
async function enterMarketAndBorrow(signer, comptroller, cTokenCollateral, cTokenBorrow, amount) {
const comptrollerContract = new ethers.Contract(
comptroller,
['function enterMarkets(address[]) returns (uint256[])'],
signer
);
const tx = await comptrollerContract.enterMarkets([cTokenCollateral]);
await tx.wait();
const cToken = new ethers.Contract(
cTokenBorrow,
['function borrow(uint256) returns (uint256)'],
signer
);
const tx2 = await cToken.borrow(amount);
await tx2.wait();
}状态管理上,建议用自定义hooks把链上数据查询和写操作分开。例如写一个useAccountLiquidityhook,轮询Comptroller的getAccountLiquidity返回账户可用借款额度,写一个useMarketMetadatahook读取每个市场的供应利率、借款利率和抵押因子。这样组件层只消费hooks返回的数据,切换到wagmi或重构时影响面也更小。
集成Governance:COMP委托与投票的前端实现
Compound的治理体系围绕COMP代币展开,核心流程是:持有者将投票权委托(delegate)给自己或他人,社区成员创建提案(propose),在延迟期后进入投票窗口,得票超过法定门槛且支持票多于反对票的提案进入时间锁(Timelock)排队执行。前端集成Governance主要涉及三块功能:投票权委托、提案列表展示、投票操作。
委托操作是参与治理的前提,注意COMP合约的委托有三种:delegate委托给指定地址,delegateBySig支持链下签名委托。前端实现一个简单的委托组件:
async function delegateVotes(signer, compAddress, delegatee) {
const comp = new ethers.Contract(
compAddress,
[
'function delegates(address) view returns (address)',
'function delegate(address)',
'function getCurrentVotes(address) view returns (uint96)'
],
signer
);
const current = await comp.delegates(await signer.getAddress());
if (current.toLowerCase() === delegatee.toLowerCase()) {
console.log('已经委托给该地址');
return;
}
const tx = await comp.delegate(delegatee);
await tx.wait();
}提案的读取需要遍历Governor Bravo合约的事件。通过ProposalCreated事件可以拿到提案ID、提案人、目标和调用数据,再结合state方法判断提案处于Pending、Active、Succeeded还是Executed阶段:
async function loadProposals(provider, governorAddress) {
const governor = new ethers.Contract(
governorAddress,
[
'event ProposalCreated(uint256,address,address[],uint256[],string[],bytes[],uint256,uint256,string)',
'function state(uint256) view returns (uint8)',
'function castVote(uint256,uint8)'
],
provider
);
const filter = governor.filters.ProposalCreated();
const events = await governor.queryFilter(filter, 18000000);
return Promise.all(events.map(async (e) => ({
id: e.args.proposalId,
proposer: e.args.proposer,
description: e.args.description,
state: await governor.state(e.args.proposalId)
})));
}投票操作调用castVote,参数中1表示支持,0表示反对。需要注意的是,投票权快照在提案创建时已经确定,提案创建后才委托的投票权对当前提案无效,UI层面应该根据用户的快照票数给出提示。另外Governor Bravo的投票期只有几天,前端要做好倒计时展示,用proposal.endBlock结合出块时间估算截止时间。
迁移后的优化建议与常见问题
迁移完成后,性能和用户体验上还有几个值得投入的点。第一,合约调用合并:页面加载时如果逐个请求市场数据,几十次RPC往返会让首屏很慢,建议用Multicall3合约把多个读请求打包成一次调用,配合React Query的缓存策略,可以把首屏数据加载时间压缩到原来的三分之一左右。
第二,错误提示要针对链上revert做翻译。Compound合约的错误都通过返回码表达而不是revert字符串,例如mint返回非零值就代表失败,前端应根据返回码映射出可读的提示,比如抵押因子不足、市场未上市等,而不是给用户抛一个原始错误。第三,gas估算失败时给出预检,在提交交易前先调用callStatic模拟执行,能提前暴露大部分问题,避免用户白白支付失败的gas费。
最后是治理数据的链下索引。如果应用需要展示历史投票记录、提案讨论等,纯靠RPC查询事件会很吃力,可以考虑用The Graph subgraph或者自建索引服务,把Governance事件落库后通过GraphQL提供给前端。这样治理页面的加载体验会有质的提升,也为后续做提案分析、代表画像等高级功能打下基础。整个迁移过程本质上是把应用从自建账本转向消费链上协议状态,前端的职责更多变成合约状态的呈现与交易编排,这也是DeFi前端开发的主流范式。
ReactCompound协议去中心化治理修改时间:2026-09-03 17:57:11