题目
后端用 OpenZeppelin StandardMerkleTree 生成白名单,但用户领取时出现 Invalid proof,有人本地验证成功后链上仍回滚。你会怎样排查?
考察目标
- 生成端、分发端与合约的编码一致性。
- root 版本、账户和证明的匹配。
- 领取状态与证明验证的不同职责。
对齐 root、叶子编码、证明与领取状态,并解释 index 绑定和本地预检的边界。
后端用 OpenZeppelin StandardMerkleTree 生成白名单,但用户领取时出现 Invalid proof,有人本地验证成功后链上仍回滚。你会怎样排查?
先核对链、合约、当前 root 和名单版本,再对齐叶子类型、参数顺序、编码及哈希算法。StandardMerkleTree 的标准叶子使用 ABI 编码后双 Keccak,但不是所有协议都如此。本地 verify 只说明证明与这份 root 匹配,交易还可能因已领取、暂停、金额或发送者条件失败。若使用 index 位图防重复,index 必须绑定进叶子或由同等强度的规则保证,不能任由调用者换 index 重放。
记录 chainId、合约地址、root、名单版本、account、amount、index 和 proof。核对前端是否用了测试网 root、旧接口缓存或其他账户的证明。并非失败几乎都来自叶子编码;错误 root、错误 proof、参数或合约状态也很常见。
StandardMerkleTree 默认按标准叶子哈希排序,使用 ABI 编码和双哈希;内部节点对采用可交换的排序哈希,与 OpenZeppelin MerkleProof 默认规则配套。其他树可能单哈希、packed 编码、不同顺序或不同哈希函数,必须遵从实际生成脚本与合约。
下面仅适用于叶子类型恰好为 (uint256 index, address account, uint256 amount) 的标准树。名单金额和 index 在 JSON 中使用十进制字符串,进入计算后转为 bigint;不要先经 Number。
import {
encodeAbiParameters,
keccak256,
parseAbiParameters,
type Address,
} from "viem";
export function claimLeaf(index: bigint, account: Address, amount: bigint) {
return keccak256(
keccak256(
encodeAbiParameters(parseAbiParameters("uint256, address, uint256"), [
index,
account,
amount,
]),
),
);
}
对应 Solidity 叶子是 keccak256(bytes.concat(keccak256(abi.encode(index, account, amount))))。index 是业务领取编号,不必等于库内部排序后的叶子位置;获取 proof 时用库的实际 entry 索引,不能混淆二者。双哈希使实际叶子哈希前像成为 32 字节,避免未哈希的 64 字节叶子被当作两个内部节点拼接的歧义。
验证成功只证明数据在树内,不会自动记录已领取。合约需要记录地址、业务领取 ID 或位图等状态,并正确执行检查与状态更新。若位图按 caller 提供的 index 标记,叶子却只绑定 account 和 amount,攻击者可能换一个 index 重用同一 proof;应在叶子绑定 index 或采用合约明确的安全关联规则。
收款地址、msg.sender、金额和 mint 数量限制依合约定义,不能从库示例推断所有 claim 都有同一个签名。允许第三方代领时,要确认收款方不能被任意替换。root 更新后的旧证明通常不再适用,但旧领取状态是否延续也取决于协议设计。
本地验证当前 root 与 proof 后,再读取已领取、暂停、余额和所需费用,模拟实际 claim 参数。提交前检查链与账户;等待期间 root 和已领取状态仍可能变化。用户拒绝保留可重试状态,RPC 失败显示未知而非无资格;交易替换或重组后重新查询领取状态,不把出现交易哈希等同已领取。
发现这道题有问题? 反馈此题