Front Running
WEB3DEV Team
# Front Running
VulnerabilidadeAs transações levam algum tempo antes de serem extraídas. Um invasor pode observar o pool de transações e enviar uma transação, incluindo-a em um bloco antes da transação original. Esse mecanismo pode ser usado para reordenar transações em benefício do invasor.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/*
Alice cria um jogo de advinhação.
Você ganha 10 ether se você encontrar o string correto que bata com o hash alvo
Vejamos como esse contrato é vulnerável para front running.
*/
/*
1. Alice implanta FindThisHash com 10 Ether.
2. Bob encontra o string correto que bate com o hash alvo. ("Ethereum")
3. Bob chama solve("Ethereum") com o preço de gás estabelecido em 15 gwei.
4. Eve está observando o pool das transações para submeter uma resposta.
5. Eve vê a resposta de Bob e chama solve("Ethereum") com um preço de gás mais
alto do que o de Bob (100 gwei).
6. A transação de Eve foi minerada antes da transação de Bob.
Eve ganha a recompensa de 10 ether.
O que aconteceu?
Transações levam algum tempo antes de serem mineradas.
Transações que ainda não foram mineradas são colocadas no pool de transações.
Transações com preço de gás mais alto são tipicamente mineradas primeiro.
Um invasor obtém a resposta de pool de transações, envia uma transação com um
preço de gás mais alto, de forma que sua transação será incluída num bloco
antes do 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");
}
}
- use o esquema commit-reveal ( https://medium.com/swlh/exploring-commit-reveal-schemes-on-ethereum-c4ff5a777db8 )
- use o envio submarino ( https://libsubmarine.org/ )
Um commitment scheme é um algoritmo criptográfico usado para permitir que alguém se comprometa com um valor, mantendo-o oculto de outras pessoas com a capacidade de revelá-lo mais tarde. Os valores em um commitment scheme são obrigatórios, o que significa que ninguém pode alterá-los depois de confirmado. O esquema tem duas fases: uma fase de confirmação na qual um valor é escolhido e especificado e uma fase de revelação na qual o valor é revelado e verificado.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/Strings.sol";
/*
Agora vamos ver como se proteger do front running usando o esquema de revelação de commit.
*/
/*
1. Alice implanta SecuredFindThisHash com 10 Ether.
2. Bob encontra a string correta que fará o hash para o hash de destino. ("Ethereum").
3. Bob então encontra o keccak256(Endereço em letras minúsculas + Solução + Segredo).
Endereço é o endereço da carteira dele em letras minúsculas, a solução é "Ethereum", Secret é como uma senha ("mysecret")
que apenas Bob sabe qual Bob usa para confirmar e revelar a solução.
keccak2566("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266Ethereummysecret") = '0xf95b1dd61edc3bd962cdea3987c6f55bcb714a02a2c3eb73bd960d6b4387fc36'
3. Bob então chama commitSolution("0xf95b1dd61edc3bd962cdea3987c6f55bcb714a02a2c3eb73bd960d6b4387fc36"),
onde ele confirma o hash da solução calculada com o preço do gás definido para 15 gwei.
4. Eve está observando o pool de transações para que a resposta seja enviada.
5. Eve vê a resposta de Bob e ele também chama commitSolution("0xf95b1dd61edc3bd962cdea3987c6f55bcb714a02a2c3eb73bd960d6b4387fc36")
com um preço de gás mais alto do que Bob (100 gwei).
6. A transação de Eve foi extraída antes da transação de Bob, mas Eve ainda não recebeu a recompensa.
Ele precisa chamar revelaSolution() com o segredo e a solução exatos, então digamos que ele está observando o pool de transações
para liderar Bob como ele fez anteriormente
7. Em seguida, Bob chama a revelaçãoSolution("Ethereum", "mysecret") com o preço do gás definido para 15 gwei;
8. Vamos considerar que Eve está observando o pool de transações, encontra a transação da solução de revelação de Bob e ele também chama
revelarSolution("Ethereum", "mysecret") com preço de gás mais alto que Bob (100 gwei)
9. Vamos considerar que desta vez também a transação de revelação de Eve foi extraída antes da transação de Bob, mas Eve será
revertido com o erro "Hash não corresponde". Como a função revelaSolution() verifica o hash usando
keccak256(msg.sender + solução + segredo). Portanto, desta vez, a véspera não consegue ganhar a recompensa.
10.Mas Bob's revelaSolution("Ethereum", "mysecret") passa na verificação de hash e recebe a recompensa de 10 ether.
*/
contract SecuredFindThisHash {
// Struct é usado para armazenar os detalhes do commit
struct Commit {
bytes32 solutionHash;
uint commitTime;
bool revealed;
}
// O hash que é necessário para ser resolvido
bytes32 public hash = 0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2;
// Endereço do vencedor
address public winner;
// Preço a ser recompensado
uint public reward;
// Status do jogo
bool public ended;
// Mapping para armazenar os detalhes do commit com endereço
mapping(address => Commit) commits;
// Modifier para verificar se o jogo está ativo
modifier gameActive {
require(!ended, "Already ended");
_;
}
constructor() payable {
reward = msg.value;
}
/*
Função de confirmação para armazenar o hash calculado usando keccak256 (endereço em minúsculas + solução + segredo).
Os usuários só podem se comprometer uma vez e se o jogo estiver ativo.
*/
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;
}
/*
Função para obter os detalhes do commit. Ele retorna uma tupla de (solutionHash, commitTime, revelaStatus);
Os usuários podem obter a solução somente se o jogo estiver ativo e eles confirmaram um 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);
}
/*
Função para revelar o commit e receber a recompensa.
Os usuários podem obter a solução de revelação somente se o jogo estiver ativo e eles confirmaram um solutionHash e ainda não foram revelados.
Ele gera um keccak256(msg.sender + solução + segredo) e o verifica com o hash confirmado anteriormente.
Os que estão na frente não poderão passar nesta verificação, pois o msg.sender é diferente.
Em seguida, a solução real é verificada usando keccak256(solution), se a solução corresponder, o vencedor é declarado,
o jogo termina e o valor da recompensa é enviado ao vencedor.
*/
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.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.");
}
}
}