Compound V3 Bulker 详解:把多笔操作打包进一笔交易
目录
- 一、Bulker 是什么
- 二、核心功能:invoke 函数
- 三、六种动作类型
- 四、非托管设计
- 五、msg.value 的安全考量(为什么独立合约)
- 六、管理员函数
- 七、非标准 ERC-20 代币处理
- 八、Gas 与 UX 的取舍
- 九、与 Comet allow 权限的关系
- 十、总结
- 十一、动手练习项目:批量操作器 MiniBulker
一、Bulker 是什么
Bulker 是 Compound V3 里的类 multicall 合约,能把多笔交易打包成一个操作。比如用户可以”在一笔交易里供应 ETH、LINK、wBTC 作抵押,并借出 USDC”。
没有 Bulker 时,这套操作要分好几笔交易(approve、supplyCollateral × 3、borrow),既麻烦又费 Gas。Bulker 把它们合成一笔,大幅改善体验。
二、核心功能:invoke 函数
Bulker 的主方法是 invoke(),接收两个参数:
function invoke(bytes32[] calldata actions, bytes[] calldata data) external payable;
- actions:一个动作类型列表(枚举值);
- data:每个动作对应的参数(ABI 编码的 bytes)。
与传统 multicall 接受任意 calldata 不同,Bulker 采用结构化方式,只允许预定义的动作类别——更安全,避免任意调用带来的风险。
三、六种动作类型
| 动作 | 含义 |
|---|---|
ACTION_SUPPLY_ASSET | 供应 ERC-20 代币作抵押 |
ACTION_SUPPLY_NATIVE_TOKEN | 直接供应 ETH(内部包装成 WETH) |
ACTION_TRANSFER_ASSET | 转移资产(配合 Comet 的 rebasing 行为) |
ACTION_WITHDRAW_ASSET | 提取 ERC-20 代币 |
ACTION_WITHDRAW_NATIVE_TOKEN | 提取 ETH(内部把 WETH 解包成 ETH) |
| 领取 COMP 奖励 | 与独立的 CometRewards.sol 交互 |
invoke 内部按顺序遍历 actions,对每个动作解码对应的 data 并执行。ETH 相关动作自动处理 WETH 的包装/解包,用户无感。
四、非托管设计
一个关键架构特性:Bulker 永远不持有用户的代币。
机制:用户给 Bulker 授权(通过 Comet 的 allow() 把 Bulker 设为操作者),Bulker 代表用户执行交易,但代币直接在用户和 Comet 之间流转。
好处:保持可组合性——其他合约能代表用户操作,而无需假定 msg.sender 就是原始存款人。Bulker 不碰资金,即使有 bug 也不会卷走用户的钱。
五、msg.value 的安全考量(为什么独立合约)
为什么 Bulker 代码要和主 Comet 合约分开?因为在循环 + delegatecall 中使用 msg.value 有安全风险。
Compound 小心确保
msg.value被扣减而非重复使用,否则会导致双花(double spending)。
举例:如果 invoke 在循环里多次用到 msg.value,而每次都读同一个 msg.value(它在整笔交易里是常量),攻击者就能用 1 份 ETH 完成多个”花费 ETH”的动作——凭空多花钱。Compound 通过跟踪剩余可用 ETH、每次扣减,防止这种重复花费。
这是 multicall + payable 的经典陷阱,文章特别推荐两个练习理解它:RareSkills Riddles 的 MultiDelegateCall、DamnVulnerableDefi 的 Free Rider 关卡。
六、管理员函数
Bulker 含 sweepToken(),允许管理员找回误转入的代币。救援卡住的 ETH 和 ERC-20 有各自的机制。因为 Bulker 非托管、平时不该持有任何代币,所以任何留在里面的都是误转,可被管理员扫出。
七、非标准 ERC-20 代币处理
Compound 稳健地兼容偏离规范的 ERC-20——特别是不返回布尔值的代币(如老版 USDT)。实现用底层 call 检查返回数据大小:
- 返回 32 字节:标准 ERC-20 响应(读布尔判断);
- 无返回数据或 revert:非标准行为;
- 成功 = 代币返回 true,或什么都不返回且不 revert;
- 失败 = 代币 revert,或明确返回 false。
合约用 IERC20NonStandard 接口处理不合规代币。这是与各种”奇葩代币”打交道的生产级写法(呼应 Module 5 Router 篇的 fee-on-transfer 处理思路)。
八、Gas 与 UX 的取舍
理论上 Compound 可以用 1 字节的动作标识符代替 32 字节的 ASCII 字符串来省 Gas,但现有设计优先安全和可维护性而非边际 Gas 节省。可读的动作标识让代码更易审计和维护,这是一个有意识的工程权衡。
九、与 Comet allow 权限的关系
Bulker 能代表用户操作,依赖 Comet 的二元授权(上一篇 cUSDC rebasing 篇讲的 allow):
comet.allow(bulkerAddress, true); // 授权 Bulker 作为操作者
授权后,Bulker 调用 Comet 的函数时,Comet 通过 hasPermission() 确认 Bulker 被用户授权,于是允许它代表用户 supply/withdraw/transfer。这就是非托管 + 可组合的权限基础:用户授权操作权,而非转移代币所有权。
十、总结
- Bulker = 结构化的 multicall,把多笔 Comet 操作合并成一笔 invoke;
- 六种动作:供应/提取 ERC-20、供应/提取 ETH(自动 WETH 包装)、转移、领奖励;
- 非托管:不持有用户代币,靠 Comet allow 授权代表用户操作,保持可组合性;
- 独立合约是为了安全处理 msg.value 循环双花风险;
- 兼容非标准 ERC-20(不返回布尔的代币);
- 用可读字符串动作标识,优先安全可维护而非极致省 Gas。
十一、动手练习项目:批量操作器 MiniBulker
项目目标
为前面练习的 MiniComet 实现一个非托管 Bulker,支持一笔交易完成多个动作(供应抵押 + 借款 + 包装 ETH),亲手处理 msg.value 循环的双花防护。部署到 Sepolia。
合约要求
MiniBulker.sol
immutable comet、immutable weth;- 定义动作常量:
ACTION_SUPPLY_ASSET、ACTION_SUPPLY_NATIVE、ACTION_WITHDRAW_ASSET、ACTION_WITHDRAW_NATIVE、ACTION_TRANSFER、ACTION_CLAIM_REWARD(用 bytes32 字符串或 uint8 枚举); invoke(bytes32[] calldata actions, bytes[] calldata data) external payable:- 维护一个局部
uint256 unusedEth = msg.value; - 遍历 actions,
for循环里decode每个 data,分派执行; - 遇到 ACTION_SUPPLY_NATIVE 时从 unusedEth 扣减对应金额(
unusedEth -= amount,不足则 revert)——这是双花防护的核心; - ETH 动作:
weth.deposit{value: amount}()后 supply,或 withdraw 后weth.withdraw再转 ETH 给用户; - 结束时把剩余 unusedEth 退还调用者;
- 维护一个局部
sweepToken(address token)/sweepEth():onlyAdmin 救援误转资产;- 所有对 comet 的操作以
msg.sender(原始用户)为受益人——依赖用户事先comet.allow(bulker, true)。
测试要求(Foundry)
test_BatchSupplyAndBorrow:一笔 invoke 完成”supply 抵押 + withdraw(借) base”,验证最终头寸正确;test_SupplyNativeWrapsToWeth:ACTION_SUPPLY_NATIVE 带 ETH,验证被包装成 WETH 存入 comet;test_MsgValueNoDoubleSpend(核心):构造两个 ACTION_SUPPLY_NATIVE 但 msg.value 只够一个,断言 revert(不能用 1 份 ETH 花两次);正常情况下两动作金额之和 == msg.value 才成功;test_RefundUnusedEth:msg.value 多于实际用量,多余部分退还调用者;test_NonCustodial:invoke 执行后 Bulker 自身余额为 0(不持有用户资产);test_RequiresAllow:用户未comet.allow(bulker, true)时 invoke 中的 comet 操作 revert;test_SweepRecoversMistransfer:误转入 Bulker 的代币能被 admin sweep 出来。
Sepolia 部署与验证步骤
- 部署 MiniComet、WETH、MiniBulker;
- 用户先
comet.allow(bulker, true)授权; - 构造 actions 数组(如 [SUPPLY_NATIVE, WITHDRAW_ASSET])和对应 data,带上 ETH 调 invoke;
- 在 Etherscan 上验证一笔交易完成了多个动作、Bulker 余额为 0、多余 ETH 已退还;
- 故意让两个 SUPPLY_NATIVE 金额之和 > msg.value,观察 revert(双花防护)。
进阶挑战(可选)
- 做 RareSkills Riddles 的 MultiDelegateCall 或 DamnVulnerableDefi 的 Free Rider,亲手利用 msg.value 循环双花漏洞,再用本练习的扣减法修复,彻底理解防护原理;
- 支持非标准 ERC-20(不返回布尔的代币):用底层 call + returndatasize 判断,复刻第七节逻辑;
- 写注释回答:为什么 Bulker 设计成非托管?如果它持有用户代币会引入哪些风险,又会破坏什么可组合性?