WEB3DEV

Cover image for Melhores Práticas do Solidity para Segurança dos Smart Contracts
Panegali
Panegali

Posted on • Atualizado em

Melhores Práticas do Solidity para Segurança dos Smart Contracts

Se você levou a sério a mentalidade de segurança de smart contract e está entendendo as idiossincrasias do EVM, é hora de considerar alguns padrões de segurança específicos da linguagem de programação Solidity. Neste resumo, vamos nos concentrar nas recomendações de desenvolvimento seguro para Solidity que também podem ser instrutivas para o desenvolvimento de smart contracts em outras linguagens.

Ok, vamos lá.

Use assert(), require(), revert() corretamente

As funções de conveniência assert e require podem ser usadas para verificar condições e lançar uma exceção se a condição não for atendida.

A função assert só deve ser usada para testar erros internos e verificar invariantes.

A função require deve ser usada para garantir condições válidas, como entradas ou variáveis ​​de estado do contrato, ou para validar valores de retorno de chamadas para contratos externos.

Seguir este paradigma permite que ferramentas de análise formal verifiquem que o opcode inválido nunca pode ser alcançado: significando que nenhuma invariante no código é violada e que o código é formalmente verificado.

pragma solidity ^0.5.0;

contract Sharer {
    function sendHalf(address payable addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0, "Even value required."); //Require() can have an optional message string
        uint balanceBeforeTransfer = address(this).balance;
        (bool success, ) = addr.call.value(msg.value / 2)("");
        require(success);
        // Since we reverted if the transfer failed, there should be
        // no way for us to still have half of the money.
        assert(address(this).balance == balanceBeforeTransfer - msg.value / 2); // used for internal error checking
        return address(this).balance;
    }
} 
Enter fullscreen mode Exit fullscreen mode

Consulte SWC-110 e SWC-123

Use modificadores apenas para verificações

O código dentro de um modificador geralmente é executado antes do corpo da função, portanto, qualquer mudança de estado ou chamada externa violará o padrão Checks-Effects-Interactions. Além disso, essas declarações também podem passar despercebidas pelo desenvolvedor, pois o código do modificador pode estar longe da declaração da função. Por exemplo, uma chamada externa no modificador pode levar ao ataque de reentrada:

contract Registry {
    address owner;

    function isVoter(address _addr) external returns(bool) {
        // Code
    }
}

contract Election {
    Registry registry;

    modifier isEligible(address _addr) {
        require(registry.isVoter(_addr));
        _;
    }

    function vote() isEligible(msg.sender) public {
        // Code
    }
}
Enter fullscreen mode Exit fullscreen mode

Nesse caso, o contrato do Registry pode fazer um ataque de reentrada chamando Election.vote() dentro de isVoter().

Observação: use modificadores para substituir verificações de condição duplicadas em várias funções, como isOwner(), caso contrário, use require ou revert dentro da função. Isso torna seu código de smart contract mais legível e mais fácil de auditar.

Cuidado com Arredondamento com Divisão Inteira

Todas as divisões de inteiros são arredondadas para o inteiro mais próximo. Se você precisar de mais precisão, considere usar um multiplicador ou armazene o numerador e o denominador.

(No futuro, Solidity terá um tipo de ponto fixo, o que tornará isso mais fácil.)

// bad
uint x = 5 / 2; // Result is 2, all integer division rounds DOWN to the nearest integer
Enter fullscreen mode Exit fullscreen mode

O uso de um multiplicador evita o arredondamento, esse multiplicador precisa ser considerado ao trabalhar com x no futuro:

// good
uint multiplier = 10;
uint x = (5 * multiplier) / 2;
Enter fullscreen mode Exit fullscreen mode

Armazenar o numerador e o denominador significa que você pode calcular o resultado do numerator/denominator off-chain:

// good
uint numerator = 5;
uint denominator = 2;
Enter fullscreen mode Exit fullscreen mode

Esteja ciente das compensações entre contratos abstratos e interfaces

Tanto as interfaces quanto os contratos abstratos fornecem uma abordagem personalizável e reutilizável para smart contracts. As interfaces, que foram introduzidas no Solidity 0.4.11, são semelhantes aos contratos abstratos, mas não podem ter nenhuma função implementada. As interfaces também possuem limitações, como não poder acessar o armazenamento ou herdar de outras interfaces, o que geralmente torna os contratos abstratos mais práticos. Embora as interfaces sejam certamente úteis para projetar contratos antes da implementação. Além disso, é importante ter em mente que, se um contrato herdar de um contrato abstrato, ele deve implementar todas as funções não implementadas por meio de substituição ou também será abstrato.

Funções fallback

Mantenha as funções de fallback simples

As funções de fallback são chamadas quando um contrato recebe uma mensagem sem argumentos (ou quando nenhuma função corresponde) e só tem acesso a 2.300 gas quando chamado de um .send()ou .transfer(). Se você deseja receber Ether de um .send()ou .transfer(), o máximo que você pode fazer em uma função de fallback é registrar um evento. Use uma função adequada se for necessário um cálculo de mais gas.

// bad
function() payable { balances[msg.sender] += msg.value; }

// good
function deposit() payable external { balances[msg.sender] += msg.value; }

function() payable { require(msg.data.length == 0); emit LogDepositReceived(msg.sender); }
Enter fullscreen mode Exit fullscreen mode

Verifique o comprimento dos dados nas funções de fallback

Como as funções de fallback não são chamadas apenas para transferências de ether simples (sem dados), mas também quando nenhuma outra função corresponde, você deve verificar se os dados estão vazios se a função de fallback for usada apenas para registrar o ether recebido. Caso contrário, os chamadores não perceberão se o seu contrato for usado incorretamente e as funções que não existem forem chamadas.

// bad
function() payable { emit LogDepositReceived(msg.sender); }

// good
function() payable { require(msg.data.length == 0); emit LogDepositReceived(msg.sender); }
Enter fullscreen mode Exit fullscreen mode

Marcar explicitamente funções a pagar e variáveis ​​de estado

A partir do Solidity 0.4.0, toda função que está recebendo ether deve usar payable modificador, caso contrário, se a transação tiver msg.value > 0 será revertida (exceto quando forçada).

Nota: Algo que pode não ser óbvio: O payable modificador se aplica apenas a chamadas de contratos externos. Se eu chamar uma função não pagável na função pagável no mesmo contrato, a função não pagável não falhará, embora msg.value ainda esteja definida.

Marcar explicitamente a visibilidade em funções e variáveis ​​de estado

Rotule explicitamente a visibilidade de funções e variáveis ​​de estado. As funções podem ser especificadas como sendo external, public, internal ou private. Por favor, entenda as diferenças entre eles, por exemplo, external pode ser suficiente em vez de public. Para variáveis ​​de estado, external não é possível. Rotular a visibilidade explicitamente facilitará a captura de suposições incorretas sobre quem pode chamar a função ou acessar a variável.

  • External funções fazem parte da interface do contrato. Uma função externa f não pode ser chamada internamente (ou seja f(), não funciona, mas this.f()funciona). As funções externas às vezes são mais eficientes quando recebem grandes matrizes de dados.
  • Public As funções fazem parte da interface do contrato e podem ser chamadas internamente ou por meio de mensagens. Para variáveis ​​de estado públicas, uma função getter automática (veja abaixo) é gerada.
  • Internal funções e variáveis ​​de estado só podem ser acessadas internamente, sem usar this.
  • Private funções e variáveis ​​de estado são visíveis apenas para o contrato em que são definidas e não em contratos derivados. Nota: Tudo o que está dentro de um contrato é visível para todos os observadores externos ao blockchain, até variáveis Private.
// bad
uint x; // the default is internal for state variables, but it should be made explicit
function buy() { // the default is public
    // public code
}

// good
uint private y;
function buy() external {
    // only callable externally or using this.buy()
}

function utility() public {
    // callable externally, as well as internally: changing this code requires thinking about both cases.
}

function internalAction() internal {
    // internal code
}

Enter fullscreen mode Exit fullscreen mode

Consulte SWC-100 e SWC-108

Bloqueie os pragmas para uma versão específica do compilador

Os contratos devem ser implantados com a mesma versão do compilador e sinalizadores com os quais foram testados mais. Bloquear o pragma ajuda a garantir que os contratos não sejam implantados acidentalmente usando, por exemplo, o compilador mais recente, que pode ter maiores riscos de bugs não descobertos. Os contratos também podem ser implantados por outros e o pragma indica a versão do compilador pretendida pelos autores originais.

// bad
pragma solidity ^0.4.4;


// good
pragma solidity 0.4.4;
Enter fullscreen mode Exit fullscreen mode

Nota: uma versão de pragma flutuante (ie. ^0.4.25) compilará bem com 0.4.26-nightly.2018.9.25, no entanto, compilações noturnas nunca devem ser usadas para compilar código para produção.

Aviso: As instruções de pragma podem flutuar quando um contrato é destinado ao consumo de outros desenvolvedores, como no caso de contratos em uma biblioteca ou pacote EthPM. Caso contrário, o desenvolvedor precisaria atualizar manualmente o pragma para compilar localmente.

Consulte SWC-103

Use eventos para monitorar a atividade do contrato

Pode ser útil ter uma maneira de monitorar a atividade do contrato após sua implantação. Uma maneira de fazer isso é observar todas as transações do contrato, no entanto, isso pode ser insuficiente, pois as chamadas de mensagens entre os contratos não são registradas na blockchain. Além disso, mostra apenas os parâmetros de entrada, não as alterações reais que estão sendo feitas no estado. Também os eventos podem ser usados ​​para acionar funções na interface do usuário.

contract Charity {
    mapping(address => uint) balances;

    function donate() payable public {
        balances[msg.sender] += msg.value;
    }
}

contract Game {
    function buyCoins() payable public {
        // 5% goes to charity
        charity.donate.value(msg.value / 20)();
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, o contrato Game fará uma chamada interna para Charity.donate(). Esta transação não aparecerá na lista de transações externas de Charity, mas apenas visível nas transações internas.

Um evento é uma maneira conveniente de registrar algo que aconteceu no contrato. Os eventos que foram emitidos permanecem na blockchain junto com os outros dados do contrato e ficam disponíveis para auditoria futura. Aqui está uma melhoria no exemplo acima, usando eventos para fornecer um histórico das doações de Charity.

contract Charity {
    // define event
    event LogDonate(uint _amount);

    mapping(address => uint) balances;

    function donate() payable public {
        balances[msg.sender] += msg.value;
        // emit event
        emit LogDonate(msg.value);
    }
}

contract Game {
    function buyCoins() payable public {
        // 5% goes to charity
        charity.donate.value(msg.value / 20)();
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, todas as transações que passam pelo contrato Charity, diretamente ou não, aparecerão na lista de eventos daquele contrato junto com o valor doado.

Nota: Prefira construções Solidity mais recentes. Prefira constructs/aliases como selfdestruct (over suicide) e keccak256 (over sha3). Padrões como require(msg.sender.send(1 ether))também podem ser simplificados para usar transfer(), como em msg.sender.transfer(1 ether). Confira o log de alterações do Solidity para obter mais alterações semelhantes.

Esteja ciente de que “Built-ins” pode ser sombreado

Atualmente, é possível sombrear os globais integrados no Solidity. Isso permite que os contratos substituam a funcionalidade de built-ins como msg e revert(). Embora isso seja intencional , pode enganar os usuários de um contrato quanto ao verdadeiro comportamento do contrato.

contract PretendingToRevert {
    function revert() internal constant {}
}

contract ExampleContract is PretendingToRevert {
    function somethingBad() public {
        revert();
    }
}
Enter fullscreen mode Exit fullscreen mode

Os usuários do contrato (e auditores) devem estar cientes do código fonte completo do smart contract de qualquer aplicativo que pretendam usar.

Evite usar tx.origin

Nunca use tx.origin para autorização, outro contrato pode ter um método que chamará seu contrato (onde o usuário tem alguns fundos por exemplo) e seu contrato autorizará essa transação já que seu endereço está em tx.origin.

contract MyContract {

    address owner;

    function MyContract() public {
        owner = msg.sender;
    }

    function sendTo(address receiver, uint amount) public {
        require(tx.origin == owner);
        (bool success, ) = receiver.call.value(amount)("");
        require(success);
    }

}

contract AttackingContract {

    MyContract myContract;
    address attacker;

    function AttackingContract(address myContractAddress) public {
        myContract = MyContract(myContractAddress);
        attacker = msg.sender;
    }

    function() public {
        myContract.sendTo(attacker, msg.sender.balance);
    }

} 
Enter fullscreen mode Exit fullscreen mode

Você deve usar msg.sender para autorização (se outro contrato chamar seu contrato msg.sender será o endereço do contrato e não o endereço do usuário que chamou o contrato).

Você pode ler mais sobre isso aqui: Documentos do Solidity

Atenção: Além do problema com a autorização, há uma chance de que tx.origin seja removido do protocolo Ethereum no futuro, então o código que usa tx.origin não será compatível com versões futuras Vitalik: 'NÃO assuma que tx.origin continuará sendo utilizável ou significativo.'

Também vale a pena mencionar que, ao usar, tx.origin você está limitando a interoperabilidade entre contratos, porque o contrato que usa tx.origin não pode ser usado por outro contrato, pois um contrato não pode ser o tx.origin.

Consulte SWC-115

Dependência do Timestamp

Há três considerações principais ao usar um *timestamp*para executar uma função crítica em um contrato, especialmente quando as ações envolvem transferência de fundos.

Manipulação do Timestamp

Esteja ciente de que o timestamp do bloco pode ser manipulado por um minerador. Considere este contrato:

uint256 constant private salt =  block.timestamp;

function random(uint Max) constant private returns (uint256 result){
    //get the best seed for randomness
    uint256 x = salt * 100/Max;
    uint256 y = salt * block.number/(salt % 5) ;
    uint256 seed = block.number/3 + (salt % 300) + Last_Payout + y;
    uint256 h = uint256(block.blockhash(seed));

    return uint256((h / x)) % Max + 1; //random number between 1 and Max
}
Enter fullscreen mode Exit fullscreen mode

Quando o contrato usa o timestamp para semear um número aleatório, o minerador pode postar um timestamp dentro de 15 segundos após o bloco ser validado, efetivamente permitindo que o minerador pré calcule uma opção mais favorável às suas chances na loteria. Os timestamps não são aleatórios e não devem ser usados ​​nesse contexto.

A regra dos 15 segundos

O Yellow Paper (especificação de referência do Ethereum) não especifica uma restrição sobre quantos blocos podem derivar no tempo, mas especifica que cada timestamp deve ser maior que o timestamp de seu pai. As implementações populares do protocolo Ethereum Geth e Parity rejeitam blocos com timestamp com mais de 15 segundos no futuro. Portanto, uma boa regra geral na avaliação do uso de timestamp é: se a escala do seu evento dependente do tempo pode variar em 15 segundos e manter a integridade, é seguro usar um arquivo block.timestamp.

Evite usar block.number como timestamp

É possível estimar um delta de tempo usando a propriedade block.number e o tempo médio de bloqueio, no entanto, isso não é uma prova futura, pois os tempos de bloqueio podem mudar (como reorganizações de fork e a bomba de dificuldade). Em uma venda que abrange dias, a regra dos 15 segundos permite obter uma estimativa de tempo mais confiável.

Consulte SWC-116

Cuidado com herança múltipla

Ao utilizar herança múltipla no Solidity, é importante entender como o compilador compõe o gráfico de herança.

contract Final {
    uint public a;
    function Final(uint f) public {
        a = f;
    }
}

contract B is Final {
    int public fee;

    function B(uint f) Final(f) public {
    }
    function setFee() public {
        fee = 3;
    }
}

contract C is Final {
    int public fee;

    function C(uint f) Final(f) public {
    }
    function setFee() public {
        fee = 5;
    }
}

contract A is B, C {
  function A() public B(3) C(5) {
      setFee();
  }
}
Enter fullscreen mode Exit fullscreen mode

Quando um contrato é implantado, o compilador linearizará a herança da direita para a esquerda (depois que a palavra chave for, os pais serão listados do mais básico ao mais derivado). Aqui está a linearização do contrato A:

Final <- B <- C <- A

A consequência da linearização resultará em um valor fee de 5, pois C é o contrato mais derivado. Isso pode parecer óbvio, mas imagine cenários em que C é capaz de ocultar funções cruciais, reordenar cláusulas booleanas e fazer com que o desenvolvedor escreva contratos exploráveis. A análise estática atualmente não levanta problemas com funções ofuscadas, portanto, deve ser inspecionada manualmente.

Para saber mais sobre segurança e herança, confira este artigo.

Para ajudar a contribuir, o Github do Solidity tem um projeto com todos os problemas relacionados à herança.

Consulte SWC-125

Use interface type em vez do endereço para type safety

Quando uma função recebe um endereço de contrato como argumento, é melhor passar uma interface ou tipo de contrato em vez de raw address. Se a função for chamada em outro lugar dentro do código fonte, o compilador fornecerá garantias adicionais de type safety

Aqui vemos duas alternativas:

contract Validator {
    function validate(uint) external returns(bool);
}

contract TypeSafeAuction {
    // good
    function validateBet(Validator _validator, uint _value) internal returns(bool) {
        bool valid = _validator.validate(_value);
        return valid;
    }
}

contract TypeUnsafeAuction {
    // bad
    function validateBet(address _addr, uint _value) internal returns(bool) {
        Validator validator = Validator(_addr);
        bool valid = validator.validate(_value);
        return valid;
    }
}
Enter fullscreen mode Exit fullscreen mode

Os benefícios de usar o contrato TypeSafeAuction acima podem ser vistos no exemplo a seguir. Se validateBet()for chamado com um address argumento ou um tipo de contrato diferente de Validator, o compilador lançará este erro:

contract NonValidator{}

contract Auction is TypeSafeAuction {
    NonValidator nonValidator;

    function bet(uint _value) {
        bool valid = validateBet(nonValidator, _value); // TypeError: Invalid type for argument in function call.
                                                        // Invalid implicit conversion from contract NonValidator
                                                        // to contract Validator requested.
    }
}
Enter fullscreen mode Exit fullscreen mode

Evite usar extcodesize para verificar contas de propriedade externa

O modificador a seguir (ou uma verificação semelhante) é frequentemente usado para verificar se uma chamada foi feita de uma conta de propriedade externa (EOA) ou de uma conta de contrato:

modifier isNotContract(address _a) { uint size; assembly { size := // bad
modifier isNotContract(address _a) {
  uint size;
  assembly {
    size := extcodesize(_a)
  }
    require(size == 0);
     _;
}
Enter fullscreen mode Exit fullscreen mode

A ideia é simples: se um endereço contém código, não é um EOA, mas uma conta de contrato. No entanto, um contrato não possui código fonte disponível durante a construção . Isso significa que enquanto o construtor está em execução, ele pode fazer chamadas para outros contratos, mas extcodesize para seu endereço retorna zero. Abaixo está um exemplo mínimo que mostra como essa verificação pode ser contornada:

contract OnlyForEOA {    
    uint public flag;

    // bad
    modifier isNotContract(address _a){
        uint len;
        assembly { len := extcodesize(_a) }
        require(len == 0);
        _;
    }

    function setFlag(uint i) public isNotContract(msg.sender){
        flag = i;
    }
}

contract FakeEOA {
    constructor(address _a) public {
        OnlyForEOA c = OnlyForEOA(_a);
        c.setFlag(1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Como os endereços de contrato podem ser pré-calculados, essa verificação também pode falhar se verificar um endereço vazio no bloco n, mas que tenha um contrato implantado em algum bloco maior que n.

Aviso: Este problema é matizado. Se o seu objetivo é impedir que outros contratos possam chamar seu contrato, o extcodesize cheque provavelmente é suficiente. Uma abordagem alternativa é verificar o valor de (tx.origin == msg.sender), embora isso também tenha desvantagens.

Pode haver outras situações em que o extcodesize cheque serve ao seu propósito. Descrever todos eles aqui está fora do escopo. Entenda os comportamentos subjacentes do EVM e use seu julgamento.


Artigo escrito por ConsenSys Diligence e traduzido por Marcelo Panegali.
O artigo original pode ser lido aqui.

Top comments (0)