23.抢先交易_1
2023-07-15 17:16:58 # 00.security

抢先交易_1

简介

与大多数区块链一样,以太坊节点汇集交易并将它们形成块。只有在矿工解决了共识机制(目前是以太坊的 Ethash PoW)后,交易才被视为 有效。解决区块的矿工还选择将池中的哪些交易包含在区块中,通常按 gasPrice每笔交易的顺序排序。这是一个潜在的攻击可能。攻击者可以观察交易池中可能包含问题解决方案的交易,并修改或撤销解决者的权限。然后攻击者可以从该交易中获取数据并创建他们自己的更高gasPrice的交易,以便他们的交易包含在原始交易之前的一个块中。

Transactions take some time before they are mined. An attacker can watch the transaction pool and send a transaction, have it included in a block before the original transaction. This mechanism can be abused to re-order transactions to the attacker’s advantage.

例子1

让我们通过一个简单的例子看看这是如何工作的。考虑FindThisHash.sol中显示的合同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/*
Alice creates a guessing game.
You win 10 ether if you can find the correct string that hashes to the target
hash. Let's see how this contract is vulnerable to front running.
*/

/*
1. Alice deploys FindThisHash with 10 Ether.
2. Bob finds the correct string that will hash to the target hash. ("Ethereum")
3. Bob calls solve("Ethereum") with gas price set to 15 gwei.
4. Eve is watching the transaction pool for the answer to be submitted.
5. Eve sees Bob's answer and calls solve("Ethereum") with a higher gas price
than Bob (100 gwei).
6. Eve's transaction was mined before Bob's transaction.
Eve won the reward of 10 ether.

What happened?
Transactions take some time before they are mined.
Transactions not yet mined are put in the transaction pool.
Transactions with higher gas price are typically mined first.
An attacker can get the answer from the transaction pool, send a transaction
with a higher gas price so that their transaction will be included in a block
before the original.
*/

contract FindThisHash {
bytes32 public constant hash =
0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2;

constructor() payable {}

function solve(string memory solution) public {
require(hash == keccak256(abi.encodePacked(solution)), "Incorrect answer");

(bool sent, ) = msg.sender.call{value: 10 ether}("");
require(sent, "Failed to send Ether");
}
}

假设这份合约包含 1,000 个以太币。可以找到以下 SHA-3 哈希原像的用户:

1
0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2

假设一位用户认为解决方案是Ethereum。他们调用solve并将 Ethereum作为参数。不幸的是,攻击者已经监视交易池中所有提交的解决方案。他们看到这个解决方案,检查其有效性,然后提交一个gasPrice比原始交易高得多的等价交易。因为更高gasPrice,矿工更加倾向于高gas的交易,从而打包。攻击者将拿走 1,000 个以太币,而解决问题的用户将一无所获。

请记住,在这种类型的“抢先交易”漏洞中,矿工激励自己进行攻击(或者可以被贿赂以收取高额费用来进行这些攻击)。不应低估攻击者本身就是矿工的可能性。

例子2

ERC20标准有一个潜在的前端运行漏洞,这是由于该approve功能而产生的。Mikhail Vladimirov 和 Dmitry Khovratovich对此漏洞(以及缓解攻击的方法)进行了很好的见解

approve():此功能允许用户允许其他用户代表他们转移代币

1
function approve(address _spender, uint256 _value) returns (bool success)

抢先漏洞发生在用户 Alice approve 她的朋友 Bob 花费 100 个代币的场景中。爱丽丝后来决定,她想撤销鲍勃对花费 100 个代币的批准,因此她创建了一个交易,将鲍勃的分配设置为 50 个代币。一直在仔细观察区块链打包池的 Bob 看到了这笔交易,并建立了自己花费 100 个代币的交易。他为自己的交易设定了比爱丽丝更高gasPrice的价格,因此他的交易优先于她的交易。因此,Bob转移了100个代币,然后Alice又再次approve了50个代币,这下就150个代币了

预防技术

有两类参与者可以执行此类抢先攻击:用户(修改gasPrice他们的交易)和矿工自己(他们可以按照他们认为合适的方式重新排序区块中的交易)。易受第一类(用户)攻击的合约比易受第二类(矿工)攻击的合约要差得多,因为矿工只能在解决区块时执行攻击,这对于任何针对特定区块的矿工来说都是不可能的. 在这里,我们将列出一些针对这两类攻击者的缓解措施。

一种方法是在 上设置上限gasPrice。这可以防止用户增加gasPrice并获得超过上限的优惠交易顺序。该措施仅防范第一类攻击者(任意用户)。在这种情况下,矿工仍然可以攻击合约,因为他们可以随心所欲地在他们的区块中订购交易,而不管 gas 价格如何。

一种更好的方法是使用 提交-显示 方案。这种方案要求用户发送加密(通常是哈希)的交易。在交易被包含在一个块中之后,用户发送一个交易来揭示已发送的数据(揭示阶段)。这种方法可以防止矿工和用户进行抢先交易,因为他们无法确定交易的内容。This method, however, cannot conceal the transaction value (which in some cases is the valuable information that needs to be hidden). The ENS smart contract allowed users to send transactions whose committed data included the amount of ether they were willing to spend. Users could then send transactions of arbitrary value. During the reveal phase, users were refunded the difference between the amount sent in the transaction and the amount they were willing to spend.

Remediation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/Strings.sol";

/*
Now Let's see how to guard from front running using commit reveal scheme.
*/

/*
1. Alice deploys SecuredFindThisHash with 10 Ether.
2. Bob finds the correct string that will hash to the target hash. ("Ethereum").
3. Bob then finds the keccak256(Address in lowercase + Solution + Secret).
Address is his wallet address in lowercase, solution is "Ethereum", Secret is like an password ("mysecret")
that only Bob knows whic Bob uses to commit and reveal the solution.
keccak2566("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266Ethereummysecret") = '0xf95b1dd61edc3bd962cdea3987c6f55bcb714a02a2c3eb73bd960d6b4387fc36'
3. Bob then calls commitSolution("0xf95b1dd61edc3bd962cdea3987c6f55bcb714a02a2c3eb73bd960d6b4387fc36"),
where he commits the calculated solution hash with gas price set to 15 gwei.
4. Eve is watching the transaction pool for the answer to be submitted.
5. Eve sees Bob's answer and he also calls commitSolution("0xf95b1dd61edc3bd962cdea3987c6f55bcb714a02a2c3eb73bd960d6b4387fc36")
with a higher gas price than Bob (100 gwei).
6. Eve's transaction was mined before Bob's transaction, but Eve has not got the reward yet.
He needs to call revealSolution() with exact secret and solution, so lets say he is watching the transaction pool
to front run Bob as he did previously
7. Then Bob calls the revealSolution("Ethereum", "mysecret") with gas price set to 15 gwei;
8. Let's consider that Eve's who's watching the transaction pool, find's Bob's reveal solution transaction and he also calls
revealSolution("Ethereum", "mysecret") with higher gas price than Bob (100 gwei)
9. Let's consider that this time also Eve's reveal transaction was mined before Bob's transaction, but Eve will be
reverted with "Hash doesn't match" error. Since the revealSolution() function checks the hash using
keccak256(msg.sender + solution + secret). So this time eve fails to win the reward.
10.But Bob's revealSolution("Ethereum", "mysecret") passes the hash check and gets the reward of 10 ether.
*/

contract SecuredFindThisHash {
// Struct is used to store the commit details
struct Commit {
bytes32 solutionHash;
uint commitTime;
bool revealed;
}

// The hash that is needed to be solved
bytes32 public hash =
0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2;

// Address of the winner
address public winner;

// Price to be rewarded
uint public reward;

// Status of game
bool public ended;

// Mapping to store the commit details with address
mapping(address => Commit) commits;

// Modifier to check if the game is active
modifier gameActive() {
require(!ended, "Already ended");
_;
}

constructor() payable {
reward = msg.value;
}

/*
Commit function to store the hash calculated using keccak256(address in lowercase + solution + secret).
Users can only commit once and if the game is active.
*/
function commitSolution(bytes32 _solutionHash) public gameActive {
Commit storage commit = commits[msg.sender];
require(commit.commitTime == 0, "Already committed");
commit.solutionHash = _solutionHash;
commit.commitTime = block.timestamp;
commit.revealed = false;
}

/*
Function to get the commit details. It returns a tuple of (solutionHash, commitTime, revealStatus);
Users can get solution only if the game is active and they have committed a solutionHash
*/
function getMySolution() public view gameActive returns (bytes32, uint, bool) {
Commit storage commit = commits[msg.sender];
require(commit.commitTime != 0, "Not committed yet");
return (commit.solutionHash, commit.commitTime, commit.revealed);
}

/*
Function to reveal the commit and get the reward.
Users can get reveal solution only if the game is active and they have committed a solutionHash before this block and not revealed yet.
It generates an keccak256(msg.sender + solution + secret) and checks it with the previously commited hash.
Front runners will not be able to pass this check since the msg.sender is different.
Then the actual solution is checked using keccak256(solution), if the solution matches, the winner is declared,
the game is ended and the reward amount is sent to the winner.
*/
function revealSolution(
string memory _solution,
string memory _secret
) public gameActive {
Commit storage commit = commits[msg.sender];
require(commit.commitTime != 0, "Not committed yet");
require(commit.commitTime < block.timestamp, "Cannot reveal in the same block");
require(!commit.revealed, "Already commited and revealed");

bytes32 solutionHash = keccak256(
abi.encodePacked(Strings.toHexString(msg.sender), _solution, _secret)
);
require(solutionHash == commit.solutionHash, "Hash doesn't match");

require(keccak256(abi.encodePacked(_solution)) == hash, "Incorrect answer");

winner = msg.sender;
ended = true;

(bool sent, ) = payable(msg.sender).call{value: reward}("");
if (!sent) {
winner = address(0);
ended = false;
revert("Failed to send ether.");
}
}
}
Prev
2023-07-15 17:16:58 # 00.security
Next