Mais de 10 padrões de segurança para você seguir quando estiver construindo smart contract na ethereum.
Como abordamos no Smart Contract Security Mindset, um desenvolvedor Ethereum vigilante sempre mantém cinco princípios em mente:
Prepare-se para falhas
Implemente cuidadosamente
Mantenha os contratos simples
Mantenha-se atualizado
Esteja ciente das idiossincrasias do EVM
Neste post, vamos mergulhar nas idiossincrasias do EVM e percorrer uma lista de padrões que você deve seguir ao desenvolver qualquer sistema de smart contract no Ethereum. Esse artigo é principalmente para desenvolvedores intermediários de Ethereum. Se você ainda está nos estágios iniciais de exploração, confira o programa de desenvolvimento de blockchain sob demanda da ConsenSys Academy.
Ok, vamos mergulhar.
Chamadas externas
Tenha cuidado ao fazer chamadas externas
Chamadas para smart contracts não confiáveis podem apresentar vários riscos ou erros inesperados. Chamadas externas podem executar código malicioso neste contrato ou em qualquer outro. Portanto, trate cada chamada externa como um potencial risco de segurança. Quando não for possível, ou remover chamadas externas indesejáveis, utilize as recomendações do restante desta seção para minimizar o perigo.
Marcar contratos não confiáveis
Ao interagir com contratos externos, nomeie suas variáveis, métodos e interfaces de contrato de uma forma que torne claro que interagir com eles é potencialmente inseguro. Isso se aplica às suas próprias funções que chamam contratos externos.
// ruim
Bank.withdraw(100); // Não está claro se confiável ou não
function makeWithdrawal(uint amount) { // Não está claro se esta função é potencialmente insegura
Bank.withdraw(amount);
}
// bom
UntrustedBank.withdraw(100); // chamada externa não confiável
TrustedBank.withdraw(100); // contrato de banco externo, mas confiável, mantido pela XYZ Corp
function makeUntrustedWithdrawal(uint amount) {
UntrustedBank.withdraw(amount);
}
Evite mudanças de estado após chamadas externas
Seja usando raw calls (no formato someAddress.call()) ou contract calls (no formato ExternalContract.someMethod()), suponha que o código malicioso possa ser executado. Mesmo que o ExternalContract não seja malicioso, o código malicioso pode ser executado por qualquer contrato que ele chame.
Um perigo específico é que o código malicioso pode desviar o fluxo de controle, levando a vulnerabilidades devido a reentrância. (Veja Reentrância para uma discussão mais completa deste problema).
Se você estiver fazendo uma chamada para um contrato externo não confiável, evite alterações de estado após a chamada. Esse padrão às vezes também é chamado checks-effects-interactions.
Veja SWC-107
Não use transfer() ou send().
.transfer() e .send() encaminham exatamente 2.300 gas para o destinatário. O objetivo dessa bolsa de gas codificada era evitar vulnerabilidades de reentrância, mas isso só faz sentido sob o pressuposto de que os custos de gas são constantes.A EIP 1884, que fazia parte do hard fork de Istambul, aumentou o custo do gas da operação SLOAD. Isso fez com que a função de fallback de um contrato custasse mais de 2.300 gas. Recomendamos parar de usar .transfer() e .send() e que, em vez disso, utilize .call().
// ruim
contract Vulnerable {
function withdraw(uint256 amount) external {
// Isso encaminha 2300 gas, o que pode não ser suficiente se o destinatário
// é um contrato e os custos do gas mudam.
msg.sender.transfer(amount);
}
}
// bom
contract Fixed {
function withdraw(uint256 amount) external {
// Isso encaminha todo o gas disponível. Não deixe de conferir o valor de retorno!
(bool success, ) = msg.sender.call.value(amount)("");
require(success, "Transfer failed.");
}
}
Observe que .call() não faz nada para mitigar os ataques de reentrância, portanto, outras precauções devem ser tomadas. Para evitar ataques de reentrância, use o padrão checks-effects-interactions.
Tratamento de erros em chamadas externas
O Solidity oferece métodos de chamada de baixo nível que funcionam em endereços brutos: address.call(), address.callcode(), address.delegatecall() e address.send(). Esses métodos de baixo nível nunca lançam uma exceção, mas retornarão false se a chamada encontrar uma exceção. Por outro lado, chamadas de contrato (por exemplo, ExternalContract.doSomething()) irão automaticamente propagar um lançamento (por exemplo, ExternalContract.doSomething() também lançará se doSomething() lançar).
Se você optar por usar os métodos de chamada de baixo nível, certifique-se de lidar com a possibilidade de a chamada falhar, verificando o valor de retorno.
// ruim
someAddress.send(55);
someAddress.call.value(55)(""); // isso é duplamente perigoso, pois ele encaminhará todo o gas restante e não verificará o resultado
someAddress.call.value(100)(bytes4(sha3("deposit()"))); // se o depósito lançar uma exceção, o call() bruto retornará apenas false e a transação NÃO será revertida
// bom
(bool success, ) = someAddress.call.value(55)("");
if(!success) {
// trata o código de falha
}
ExternalContract(someAddress).deposit.value(100)();
Veja SWC-104
Favorecer pull over push para chamadas externas
As chamadas externas podem falhar acidentalmente ou deliberadamente. Para minimizar os danos causados por essas falhas, geralmente é melhor isolar cada chamada externa em sua própria transação que pode ser iniciada pelo destinatário da chamada. Isso é especialmente relevante para pagamentos, onde é melhor permitir que os usuários retirem fundos em vez de enviar fundos para eles automaticamente. (Isso também reduz a chance de problemas com o limite de gas.) Evite combinar várias transferências de ether em uma única transação.
// ruim
contract auction {
address highestBidder;
uint highestBid;
function bid() payable {
require(msg.value >= highestBid);
if (highestBidder != address(0)) {
(bool success, ) = highestBidder.call.value(highestBid)("");
require(success); // se essa chamada falhar consistentemente, ninguém mais poderá fazer lances
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
// bom
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() payable external {
require(msg.value >= highestBid);
if (highestBidder != address(0)) {
refunds[highestBidder] += highestBid; // registrar o reembolso que este usuário pode reivindicar
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(refund)("");
require(success);
}
}
Consulte [SWC-128](https://swcregistry.io/docs/SWC-128)
**Não delegue chamada para código não confiável**
A função _delegatecall_ chama funções de outros contratos como se pertencessem ao contrato do chamador. Assim, o chamador pode alterar o estado do endereço de chamada. Isso pode ser inseguro. Um exemplo abaixo mostra como o uso de _delegatecall_ pode levar à destruição do contrato e à perda de seu saldo.
contract Destructor
{
function doWork() external
{
selfdestruct(0);
}
}
contract Worker
{
function doWork(address _internalWorker) public
{
// unsafe
_internalWorker.delegatecall(bytes4(keccak256("doWork()")));
}
}
Se Worker.doWork() for chamado com o endereço do contract Destructor implantado como argumento, o contrato Worker se autodestruirá. Delegue a execução apenas a contratos confiáveis e nunca a um endereço fornecido pelo usuário.
Aviso
Não assuma que os contratos são criados com saldo zero. Um invasor pode enviar ether para o endereço de um contrato antes que ele seja criado. Os contratos não devem presumir que seu estado inicial contém um saldo zero. Consulte a edição 61 para obter mais detalhes.
Veja SWC-112
Lembre-se que o ether pode ser enviado à força para uma conta
Cuidado com a codificação de um invariante que verifica estritamente o saldo de um contrato.
Um invasor pode enviar ether à força para qualquer conta. Isso não pode ser evitado (nem mesmo com uma função de fallback que faz um revert()).
O invasor pode fazer isso criando um contrato, financiando-o com 1 wei e invocando selfdestruct(victimAddress). Nenhum código é invocado em vitimaAddress, portanto, não pode ser evitado. Isso também vale para recompensas em bloco que são enviadas para o endereço do minerador, que pode ser qualquer endereço arbitrário.
Além disso, como os endereços do contrato podem ser pré-computados, o ether pode ser enviado para um endereço antes da implantação do contrato.
Consulte SWC-132
Lembre-se de que os dados on-chain são públicos
Muitos aplicativos exigem que os dados enviados sejam privados até algum momento para funcionar. Jogos (por exemplo, pedra-papel-tesoura on-chain) e mecanismos de leilão (por exemplo, leilões de Vickrey) são duas categorias principais de exemplos. Se você estiver criando um aplicativo em que a privacidade é um problema, evite exigir que os usuários publiquem informações muito cedo. A melhor estratégia é usar esquemas de commit com fases separadas: primeiro commit usando o hash dos valores e em uma fase posterior revelando os valores.
Exemplos:
Em pedra, papel e tesoura, exija que ambos os jogadores enviem um hash de seu movimento pretendido primeiro e, em seguida, exija que ambos os jogadores enviem seu movimento; se o movimento enviado não corresponder ao hash, jogue-o fora.
Em um leilão, exija que os jogadores enviem um hash do valor do lance em uma fase inicial (com um depósito maior que o valor do lance) e, em seguida, envie o valor do lance do leilão na segunda fase.Ao desenvolver um aplicativo que dependa de um gerador de números aleatórios, a ordem deve ser sempre (1) jogadores enviam movimentos, (2) números aleatórios gerados, (3) jogadores pagos. Muitas pessoas estão pesquisando ativamente geradores de números aleatórios; as melhores soluções atuais incluem cabeçalhos de bloco Bitcoin (verificados por http://btcrelay.org/), esquemas hash-commit-reveal (ou seja, uma parte gera um número, publica seu hash para “comprometer-se” com o valor e depois revela o valor) e RANDAO. Como o Ethereum é um protocolo determinístico, você não pode usar nenhuma variável no protocolo como um número aleatório imprevisível. Também esteja ciente que os mineradores estão, de certa forma, no controle do valor block.blockhash()*.
Cuidado com a possibilidade de que alguns participantes possam "ficar offline" e não retornar
Não faça com que os processos de reembolso ou reclamação dependam de uma parte específica realizando uma ação específica sem outra maneira de retirar os fundos. Por exemplo, em um jogo de pedra-papel-tesoura, um erro comum é não fazer um pagamento até que ambos os jogadores enviem seus movimentos; no entanto, um jogador mal-intencionado pode “afligir” o outro simplesmente nunca enviando seu lance – na verdade, se um jogador vê o lance revelado do outro jogador e determina que ele perdeu, ele não tem nenhuma razão para enviar seu próprio lance. Esta questão também pode surgir no contexto da liquidação do canal estatal. Quando tais situações são um problema, (1) forneça uma maneira de contornar os participantes não participantes, talvez por um limite de tempo, e (2) considere adicionar um incentivo econômico adicional para os participantes enviarem informações em todas as situações em que estiverem deveria fazê-lo.
Cuidado com a negação do inteiro com sinal negativo
O Solidity oferece vários tipos para trabalhar com inteiros com sinal. Como na maioria das linguagens de programação, no Solidity um inteiro assinado com N bits pode representar valores de -2^(N-1) a 2^(N-1)-1. Isso significa não haver equivalente positivo para MIN_INT. A negação é implementada como encontrar o complemento de dois de um número, de modo que a negação do número mais negativo resultará no mesmo número. Isso é verdade para todos os tipos inteiros assinados no Solidity (int8, int16, …, int256).
contrato Negação {
function negate8(int8 _i) public pure return(int8) {
return -_i;
}
function negate16(int16 _i) public pure return(int16) {
return -_i;
}
int8 public a = negate8(-128); // -128
int16 public b = negate16(-128); // 128
int16 public c = negate16(-32768); // -32768
}
Uma maneira de lidar com isso é verificar o valor de uma variável antes da negação e lançar se for igual a MIN_INT. Outra opção é certificar-se de que o número mais negativo nunca será alcançado usando um tipo com maior capacidade (por exemplo, int32 em vez de int16).
Um problema semelhante com tipos int ocorre quando MIN_INT é multiplicado ou dividido por -1.
O seu código blockchain é seguro?
Esperamos que essas recomendações tenham sido úteis. Se você e sua equipe estão se preparando para o lançamento ou mesmo no início do ciclo de vida de desenvolvimento e precisam que seus smart contracts sejam verificados, sinta-se à vontade para entrar em contato com nossa equipe de engenheiros de segurança da ConsenSys Diligence. Estamos aqui para ajudá-lo a lançar e manter seus aplicativos Ethereum com 100% de confiança.
Mais sobre este tópico
Este artigo é uma tradução do ConsenSys feita por Arnaldo Campos. Você pode encontrar o artigo original aqui.
Top comments (0)