Analizzatori di Solidity

L’analisi statica del codice è un processo di debug del codice che si effettua esaminandolo senza eseguirlo.

Il plugin Analizzatori di Solidity riunisce tre strumenti di analisi per eseguire analisi statiche sui contratti intelligenti di Solidity. Ogni strumento controlla le vulnerabilità di sicurezza e le procedure di sviluppo scorrette, tra le altre cose. Può essere attivato dal Gestore dei plugin di Remix.

Solidity Analyzers can also be loaded by clicking on the Solidity icon in the featured plugins section of Remix’s home tab. This button loads the following plugins: Solidity Compiler, Solidity Unit Testing, and Static Analyzers.

«Analizzatori di Solidity» utilizza questi strumenti:

NOTA: Slither può essere usato solo quando Remix è collegato al filesystem del computer locale con Remixd.

Come si usa

Un contratto deve essere compilato prima di poter eseguire l’analisi.

Nella parte superiore del pannello, selezionate gli strumenti che desiderate utilizzare.

Errori & Avvertimenti

Per impostazione predefinita, Analizzatori di Solidity mostra sia gli errori che gli avvertimenti. Il numero combinato di errori e avvertimenti è indicato nel riquadro della scheda Strumenti.

Se si seleziona Nascondi avvertimenti, gli avvertimenti saranno nascosti e si vedranno solo gli errori.

NOTA: Analisi di Remix non segnala gli errori - mostra solo gli avvertimenti; pertanto, se si seleziona Nascondere gli avvertimenti, nella scheda Analisi di Remix non verrà visualizzato nulla.

Avvertimenti da librerie esterne

Per impostazione predefinita, gli avvertimenti dalle librerie esterne non vengono mostrati. Se si seleziona la casella Mostra avvertimenti per le librerie esterne, gli strumenti analizzeranno anche le librerie esterne alla ricerca di avvertimenti.

Slither

Per eseguire Slither con questo plugin, è necessario collegare Remix IDE al proprio filesystem con Remixd. Una volta che Remixd è in esecuzione, Slither viene caricato automaticamente.

Solhint

Il linter Solhint può essere eseguito senza collegare Remix al proprio filesystem.

Analisi di Remix

Analisi di Remix ha 4 categorie: Sicurezza, Carburante & Economia, ERC e Varie.

Ecco l’elenco dei moduli di ciascuna categoria insieme al codice di esempio che dovrebbe essere evitato o usato con molta attenzione durante lo sviluppo:

Categoria: Sicurezza

  • Origine della transazione: si usa “tx.origin”

tx.origin è utile solo in casi eccezionali. Se lo si usa per l’autenticazione, di solito lo si vuole sostituire con «msg.sender», perché altrimenti qualsiasi contratto invocato può agire per conto dell’utente.

Esempio:

require(tx.origin == owner);
  • Controlla gli effetti: potenziali bug di rientranza

La potenziale violazione dello schema Controlli-Effetti-Interazione può portare alla vulnerabilità della rientranza.

Esempio:

// sending ether first
msg.sender.transfer(amount);

// updating state afterwards
balances[msg.sender] -= amount;
  • Assemblaggio in linea: utlizzo dell’Assemblaggio in linea

L’uso dell’assemblaggio in linea è consigliato solo in rari casi.

Esempio:

assembly {
            // retrieve the size of the code, this needs assembly
            let size := extcodesize(_addr)
}
  • Block timestamp: Semantics may be unclear

now non significa ora corrente. now è un alias per block.timestamp. block.timestamp può essere influenzato in una certa misura dai validatori, fate attenzione.

Esempio:

// using now for date comparison
if(startDate > now)
    isStarted = true;

// using block.timestamp
uint c = block.timestamp;
  • Low level calls: Semantics may be unclear

L’uso di call, callcode o delegatecall di basso livello dovrebbe essere evitato quando possibile. send non lancia un’eccezione quando non ha successo, assicuratevi di gestire il caso di fallimento di conseguenza. Utilizzate transfer ogni volta che il fallimento del trasferimento di Ether deve far riavvolgere l’intera transazione.

Esempio:

x.call('something');
x.send(1 wei);
  • Utilizzo di blockhash: La semantica potrebbe non essere chiara

blockhash è usato per accedere agli ultimi 256 hash dei blocchi. Un validatore calcola l’hash del blocco «sommando» le informazioni del blocco correntemente processato. Sommando le informazioni in modo intelligente, un validatore può cercare di influenzare l’esito di una transazione nel blocco attuale.

Esempio:

bytes32 b = blockhash(100);
  • Autodistruzione: Fai attenzione ai contratti «caller»

selfdestruct può bloccare inaspettatamente le invocazioni ai contratti. Fare particolare attenzione se si prevede che questo contratto sia usato da altri contratti (ad esempio, contratti di libreria, interazioni). L’autodistruzione del contratto invocante può lasciare gli invocatori in uno stato inutilizzabile.

Esempio:

selfdestruct(address(0x123abc..));

Categoria: Carburante & Economia

  • Costi del carburante: Requisiti di carburante delle funzioni troppo elevati

Se il fabbisogno di carburante di una funzione è superiore al limite di carburante del blocco, la funzione non può essere eseguita. Si prega di evitare i loop nelle funzioni o le azioni che modificano grandi aree di memoria

Esempio:

for (uint8 proposal = 0; proposal < proposals.length; proposal++) {
    if (proposals[proposal].voteCount > winningVoteCount) {
        winningVoteCount = proposals[proposal].voteCount;
        winningProposal = proposal;
    }
}
  • «This» nelle invocazioni locali: Invocazione di funzioni locali tramite “this”

Non usare mai this per invocare funzioni nello stesso contratto, perché consuma solo più carburante delle normali invocazioni locali.

Esempio:

contract test {
    
    function callb() public {
        address x;
        this.b(x);
    }
    
    function b(address a) public returns (bool) {}
}
  • Cancellazione su una matrice dinamica: Usare require/assert in modo appropriato

L’operazione delete, se applicata a una matrice con dimensioni dinamiche in Solidity, genera codice per cancellare ciascuno degli elementi contenuti. Se la matrice è grande, questa operazione può superare il limite di carburante del blocco e generare un’eccezione OOG. Anche gli oggetti contenuti di dimensioni dinamiche possono produrre gli stessi risultati.

Esempio:

contract arr {
    uint[] users;
    function resetState() public{
        delete users;
    }
}
  • Ciclo for su una matrice dinamica: Le iterazioni dipendono dalla dimensione della matrice dinamica

I cicli che non hanno un numero fisso di iterazioni, ad esempio quelli che dipendono dai valori di memorizzazione, devono essere utilizzati con attenzione: a causa del limite di carburante del blocco, le transazioni possono consumare solo una certa quantità di carburante. Il numero di iterazioni in un ciclo può crescere oltre il limite di carburante dei blocchi, il che può bloccare il contratto completo a un certo punto. Inoltre, l’uso di cicli non limitati può comportare molti costi di carburante evitabili. Verifica attentamente il numero massimo di oggetti che potete puoi a queste funzioni per farle funzionare correttamente.

Esempio:

contract forLoopArr {
    uint[] array;

    function shiftArrItem(uint index) public returns(uint[] memory) {
        for (uint i = index; i < array.length; i++) {
            array[i] = array[i+1];
        }
        return array;
    }
}
  • Trasferimento di Ether in un ciclo: Trasferire Ether in un ciclo for/while/do-while

Il pagamento di Ether non dovrebbe essere effettuato in un ciclo. A causa del limite di gas dei blocchi, le transazioni possono consumare solo una certa quantità di carburante. Il numero di iterazioni in un ciclo può crescere oltre il limite di carburante dei blocchi, causando lo stallo del contratto completo a un certo punto. Se necessario, assicurarsi che il numero di iterazioni sia basso e che ci si fidi di ogni indirizzo coinvolto.

Esempio:

contract etherTransferInLoop {
    address payable owner;

    function transferInForLoop(uint index) public  {
        for (uint i = index; i < 100; i++) {
            owner.transfer(i);
        }
    }

    function transferInWhileLoop(uint index) public  {
        uint i = index;
        while (i < 100) {
            owner.transfer(i);
            i++;
        }
    }
}

Categoria: ERC

  • ERC20: “decimals” dovrebbe essere “uint8”

ERC20 La funzione decimals dei contratti deve avere uint8 come tipo di restituzione.

Esempio:

contract EIP20 {

    uint public decimals = 12;
}

Categoria: Varie

  • Funzioni Constant/View/Pure: Funzioni potenzialmente constant/view/pure

Mette in guardia riguardo a metodi che potenzialmente dovrebbero essere constant/view/pure, ma non lo sono.

Esempio:

function b(address a) public returns (bool) {
        return true;
}
  • Nomi di variabili simili: I nomi delle variabili sono troppo simili

Mette in guardia sull’uso di nomi di variabili simili.

Esempio:

// Variables have very similar names voter and voters.
function giveRightToVote(address voter) public {
    require(voters[voter].weight == 0);
    voters[voter].weight = 1;
}
  • Nessun ritorno: Una funzione con “returns” non restituisce nulla

Mette in guardia sui metodi che definiscono un tipo di ritorno, ma che non restituiscono mai esplicitamente un valore.

Esempio:

function noreturn(string memory _dna) public returns (bool) {
       dna = _dna;
   }
  • Condizioni di guardia: Usare i termini “require” e “assert” in modo appropriato

Usa assert(x) se non vuoi mai e poi mai che x sia falso, in nessuna circostanza (a parte un bug nel tuou codice). Usa require(x) se x può essere falso, ad esempio a causa di un input non valido o di un componente esterno che non funziona.

Esempio:

assert(a.balance == 0);
  • Risultato non utilizzato: Il risultato di un’operazione non utilizzato

Un’operazione binaria produce un valore che non viene utilizzato di seguito. Ciò è spesso causato dalla confusione tra assegnazione (=) e confronto (==).

Esempio:

c == 5;
or
a + b;
  • String Length: Bytes length != String length

I byte e la lunghezza delle stringhe non sono la stessa cosa, poiché si presume che le stringhe siano codificate in UTF-8 (secondo la definizione ABI), quindi un carattere non è necessariamente codificato in un byte di dati.

Esempio:

function length(string memory a) public pure returns(uint) {
    bytes memory x = bytes(a);

    return x.length;
}
  • Eliminazione da una matrice dinamica: “delete” su una matrice lascia uno spazio vuoto

L’uso di delete su una matrice lascia uno spazio vuoto. La lunghezza della matrice rimane invariata. Se si vuole rimuovere la posizione vuota, è necessario spostare manualmente gli elementi e aggiornare la proprietà «lunghezza».

Esempio:

contract arr {
    uint[] array = [1,2,3];

    function removeAtIndex() public returns (uint[] memory) {
        delete array[1];
        return array;
    }
}
  • Dati Troncati: La Divisione su valori int/uint tronca il risultato

La divisione di valori interi produce nuovamente un valore intero. Ciò significa che, ad esempio, 10 / 100 = 0 invece di 0,1, poiché il risultato è di nuovo un intero. Questo non vale per la divisione di valori (solo) letterali, poiché questi producono costanti razionali.

Esempio:

function contribute() payable public {
    uint fee = msg.value * uint256(feePercentage / 100);
    fee = msg.value * (p2 / 100);
}

Analizzatore di Remix

remix-analyzer è la libreria che sta alla base dello strumento di Analisi di Remix.

remix-analyzer è un pacchetto NPM. Può essere usato come libreria in una soluzione che supporta node.js. Trova maggiori informazioni su questo tipo di utilizzo in remix-analyzer repository