Com este post, inicio uma série de artigos sobre a segurança dos contratos inteligentes da Ethereum. Eu acho que esse tópico é muito relevante, já que o número de desenvolvedores está crescendo como uma avalanche, e não há ninguém para salvar do "rake". Tchau - traduções ...
1. Digitalização de contratos do Ethereum ao vivo quanto a erro de envio não verificado
Original - Digitalização de contratos ao vivo do Ethereum para o "Envio não verificado ..."
Autores:
Zikai Alex Wen e
Andrew MillerSabe-se que a programação de contratos inteligentes do Ethereum é propensa a erros [1] . Recentemente, vimos que vários
contratos inteligentes de última geração, como King of the Ether e DAO-1.0, continham vulnerabilidades causadas por erros de programação.
Desde março de 2015, programadores inteligentes de contratos são avisados sobre os perigos específicos da programação que podem surgir quando os contratos enviam mensagens um para o outro [6] .
Vários guias de programação recomendam como evitar erros comuns (nos documentos oficiais do Ethereum [3] e em um guia independente da UMD [2] ). Embora esses perigos sejam compreensíveis o suficiente para evitá-los, as consequências de tal erro são terríveis: o dinheiro pode ser bloqueado, perdido ou roubado.
Quão comuns são os erros resultantes desses perigos? Existem contratos de blockchain Ethereum mais vulneráveis, mas vivos? Neste artigo, respondemos a essa pergunta analisando contratos no blockchain ao vivo da Ethereum usando a nova ferramenta de análise que desenvolvemos.
Qual é o erro de envio não verificado?
Para enviar um contrato de tempo de antena para outro endereço, a maneira mais fácil é usar a palavra - chave send . Isso atua como um método definido para cada objeto. Por exemplo, o seguinte snippet de código pode ser encontrado em um contrato inteligente que implementa um jogo de tabuleiro.
/*** Listing 1 ***/ if (gameHasEnded && !( prizePaidOut ) ) { winner.send(1000); // prizePaidOut = True; }
O problema aqui é que o método de envio pode falhar. Se não funcionar, o vencedor não receberá o dinheiro, no entanto, a variável awardPaidOut será definida como True.
Existem dois casos diferentes em que a função winner.send () pode falhar. Analisaremos a diferença entre eles mais tarde. O primeiro caso é que o endereço do vencedor é um contrato (não uma conta de usuário) e o código desse contrato gera uma exceção (por exemplo, se ele usa muito "gás"). Nesse caso, talvez neste caso seja um "erro do vencedor". O segundo caso é menos óbvio. A máquina virtual Ethereum possui um recurso limitado chamado " pilha de chamadas " (profundidade da pilha de chamadas) e esse recurso pode ser usado por outro código de contrato que foi executado anteriormente em uma transação. Se a pilha de chamadas já tiver sido usada no momento em que o comando send for executado, o comando falhará, independentemente de como o vencedor seja determinado. O prêmio do vencedor será destruído sem culpa alguma!
Como esse erro pode ser evitado?
A documentação do Ethereum contém um breve aviso sobre esse perigo potencial
[3] : "Existe algum perigo ao usar o
envio - a transmissão falha se a profundidade da pilha de chamadas for 1024 (isso sempre pode ser causado pelo chamador) e também falhará se o destinatário "gas" termina. Portanto, para garantir uma transmissão segura, sempre verifique o valor de retorno do
envio, ou melhor ainda: use um modelo no qual o destinatário retire dinheiro. "
Duas frases. O primeiro é verificar o valor de retorno do envio para ver se ele foi concluído com êxito. Se não for esse o caso, lance uma exceção para reverter o estado.
/*** Listing 2 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (winner.send(1000)) prizePaidOut = True; else throw; }
Essa é uma correção adequada para o exemplo atual, mas nem sempre a decisão correta. Suponha que modifiquemos nosso exemplo para que, quando o jogo terminar, o vencedor e o perdedor revertam suas fortunas. Uma aplicação óbvia de uma solução "formal" seria a seguinte:
/*** Listing 3 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (winner.send(1000) && loser.send(10)) prizePaidOut = True; else throw; }
No entanto, isso é um erro, pois apresenta uma vulnerabilidade adicional. Embora esse código proteja o vencedor de um ataque de pilha de chamada , ele também torna o vencedor e o perdedor vulneráveis um ao outro. Nesse caso, queremos impedir um ataque de pilha de chamada , mas continuemos a execução se o comando send falhar por algum motivo.
Portanto, mesmo a melhor prática (recomendada no Guia do programador do Ethereum e Serpent, embora se aplique igualmente ao Solidity), é verificar a presença de um recurso de pilha de chamada . Podemos definir uma macro callStackIsEmpty () , que retornará um erro se e somente se o pilha de chamadas estiver vazia.
/*** Listing 4 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (callStackIsEmpty()) throw; winner.send(1000) loser.send(10) prizePaidOut = True; }
Melhor ainda, a recomendação da documentação do Ethereum - "Use um modelo no qual o destinatário recebe o dinheiro" é um pouco enigmática, mas tem uma explicação. A sugestão é reorganizar seu código para que o efeito da falha no envio seja isolado e afete apenas um destinatário por vez. A seguir, é apresentado um exemplo dessa abordagem. No entanto, essa dica também é um anti-padrão. Ele aceita a responsabilidade de verificar a pilha de chamadas para os próprios destinatários, o que torna possível cair na mesma armadilha.
/*** Listing 5 ***/ if (gameHasEnded && !( prizePaidOut ) ) { accounts[winner] += 1000 accounts[loser] += 10 prizePaidOut = True; } ... function withdraw(amount) { if (accounts[msg.sender] >= amount) { msg.sender.send(amount); accounts[msg.sender] -= amount; } }
Muitos contratos inteligentes altamente desenvolvidos são vulneráveis. A loteria Rei do Ar do Trono é o caso mais famoso desse erro [4] . Esse erro não foi percebido até que a quantia de 200 éteres (no valor de mais de 2000 dólares americanos ao preço de hoje) não conseguiu o vencedor legítimo da loteria. O código correspondente em King of the Ether é semelhante ao código da Listagem 2. Felizmente, nesse caso, o desenvolvedor do contrato conseguiu usar a função não relacionada no contrato como uma "substituição manual" para liberar os fundos bloqueados. Um administrador menos escrupuloso poderia usar a mesma função para roubar a transmissão!
Continuação da
digitalização de contratos ao vivo no Ethereum para erro de envio não verificado. Parte 2